Compare commits

..

6 Commits

Author SHA1 Message Date
chen08209
b20d9edec2 Fix url validate issues
Fix check ip performance problem

Optimize resources page
2024-07-07 13:37:08 +08:00
chen08209
5c3a0c576d Add ua selector
Support modify test url

Optimize android proxy

Fix the error that async proxy provider could not selected the proxy
2024-07-04 09:55:06 +08:00
chen08209
6dcb466fd3 Fix android proxy error 2024-07-01 20:57:24 +08:00
chen08209
acbcec358b Fix submit error 2024-07-01 19:51:11 +08:00
chen08209
a923549ddf Add windows tun
Optimize android proxy

Optimize change profile

Update application ua

Optimize delay test
2024-07-01 19:41:57 +08:00
chen08209
07bd21580b Fix android repeated request notification issues 2024-06-28 21:16:47 +08:00
67 changed files with 2386 additions and 1237 deletions

View File

@@ -21,11 +21,25 @@ jobs:
os: macos-13
steps:
- name: Setup Mingw64
if: startsWith(matrix.platform,'windows')
uses: msys2/setup-msys2@v2
with:
msystem: mingw64
install: mingw-w64-x86_64-gcc
update: true
- name: Set Mingw64 Env
if: startsWith(matrix.platform,'windows')
run: |
echo "${{ runner.temp }}\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Check Matrix
run: |
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
- name: Checkout
uses: actions/checkout@v4
@@ -52,10 +66,10 @@ jobs:
if: startsWith(matrix.platform,'android')
run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
- name: Setup Go
uses: actions/setup-go@v5

View File

@@ -22,8 +22,10 @@
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:extractNativeLibs="true"
android:label="FlClash">
android:label="FlClash"
tools:targetApi="n">
<activity
android:name="com.follow.clash.MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View File

@@ -44,6 +44,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
private var props: Props? = null
private lateinit var title: String
private lateinit var content: String
var isBlockNotification: Boolean = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
@@ -135,7 +136,6 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
GlobalState.runTime = null;
}
@SuppressLint("ForegroundServiceType")
private fun startForeground() {
if (GlobalState.runState.value != RunState.START) return
flClashVpnService?.startForeground(title, content)
@@ -152,13 +152,14 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
if (permission == PackageManager.PERMISSION_GRANTED) {
startForeground()
} else {
activity?.let {
ActivityCompat.requestPermissions(
it,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
if (isBlockNotification) return
if (activity == null) return
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
} else {
startForeground()
@@ -192,11 +193,14 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
grantResults: IntArray
): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startForeground()
isBlockNotification = true
if (grantResults.isNotEmpty()) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startForeground()
}
}
}
return true;
return false;
}

View File

@@ -47,10 +47,6 @@ class FlClashVpnService : VpnService() {
"192.168.*"
)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
fun start(port: Int, props: Props?) {
fd = with(Builder()) {
addAddress("172.16.0.1", 30)

View File

@@ -7,4 +7,8 @@
<certificates src="user" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>

View File

@@ -4,6 +4,7 @@ import "C"
import (
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
ap "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
@@ -20,6 +21,7 @@ import (
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
)
@@ -59,11 +61,17 @@ type ruleProviderSchema struct {
Interval int `provider:"interval,omitempty"`
}
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
}
type GenerateConfigParams struct {
ProfilePath *string `json:"profile-path"`
Config *config.RawConfig `json:"config" `
IsPatch *bool `json:"is-patch"`
IsCompatible *bool `json:"is-compatible"`
ProfilePath *string `json:"profile-path"`
Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"`
}
type ChangeProxyParams struct {
@@ -170,9 +178,9 @@ func getRawConfigWithPath(path *string) *config.RawConfig {
}
}
func decorationConfig(profilePath *string, cfg config.RawConfig, compatible bool) *config.RawConfig {
func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig {
prof := getRawConfigWithPath(profilePath)
overwriteConfig(prof, cfg, compatible)
overwriteConfig(prof, cfg)
return prof
}
@@ -322,7 +330,7 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
*rule = computedRule
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig, compatible bool) {
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = ""
targetConfig.Interface = ""
@@ -340,8 +348,8 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Mode = patchConfig.Mode
targetConfig.Tun.Enable = patchConfig.Tun.Enable
targetConfig.Tun.Device = patchConfig.Tun.Device
//targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
//targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false
if targetConfig.DNS.Enable == false {
@@ -352,7 +360,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
//} else if runtime.GOOS == "windows" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
//}
if compatible == false {
if configParams.IsCompatible == false {
targetConfig.ProxyProvider = make(map[string]map[string]any)
targetConfig.RuleProvider = make(map[string]map[string]any)
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
@@ -388,16 +396,48 @@ func patchConfig(general *config.General) {
resolver.DisableIPv6 = !general.IPv6
}
func applyConfig(isPatch bool) {
func patchSelectGroup() {
mapping := configParams.SelectedMap
if mapping == nil {
return
}
for name, proxy := range tunnel.ProxiesWithProviders() {
outbound, ok := proxy.(*adapter.Proxy)
if !ok {
continue
}
selector, ok := outbound.ProxyAdapter.(outboundgroup.SelectAble)
if !ok {
continue
}
selected, exist := mapping[name]
if !exist {
continue
}
selector.ForceSet(selected)
}
}
var applyLock sync.Mutex
func applyConfig() {
applyLock.Lock()
defer applyLock.Unlock()
cfg, err := config.ParseRawConfig(currentConfig)
if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
if isPatch {
if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
}
if configParams.IsPatch {
patchConfig(cfg.General)
} else {
executor.Shutdown()
runtime.GC()
hub.UltraApplyConfig(cfg, true)
patchSelectGroup()
}
}

View File

@@ -14,6 +14,7 @@ const (
Process MessageType = "process"
Request MessageType = "request"
Run MessageType = "run"
Loaded MessageType = "loaded"
)
type Message struct {

View File

@@ -31,6 +31,8 @@ import (
var currentConfig = config.DefaultRawConfig()
var configParams = ConfigExtendedParams{}
var isInit = false
//export initClash
@@ -96,13 +98,10 @@ func updateConfig(s *C.char, port C.longlong) {
bridge.SendToPort(i, err.Error())
return
}
prof := decorationConfig(params.ProfilePath, *params.Config, *params.IsCompatible)
configParams = params.Params
prof := decorationConfig(params.ProfilePath, params.Config)
currentConfig = prof
if *params.IsPatch {
applyConfig(true)
} else {
applyConfig(false)
}
applyConfig()
bridge.SendToPort(i, "")
}()
}
@@ -150,7 +149,7 @@ func getProxies() *C.char {
}
//export changeProxy
func changeProxy(s *C.char) bool {
func changeProxy(s *C.char) {
paramsString := C.GoString(s)
go func() {
var params = &ChangeProxyParams{}
@@ -158,22 +157,24 @@ func changeProxy(s *C.char) bool {
if err != nil {
log.Infoln("Unmarshal ChangeProxyParams %v", err)
}
groupName := *params.GroupName
proxyName := *params.ProxyName
proxies := tunnel.ProxiesWithProviders()
proxy := proxies[*params.GroupName]
if proxy == nil {
group, ok := proxies[groupName]
if !ok {
return
}
log.Infoln("change proxy %s", proxy.Name())
adapterProxy := proxy.(*adapter.Proxy)
adapterProxy := group.(*adapter.Proxy)
selector, ok := adapterProxy.ProxyAdapter.(*outboundgroup.Selector)
if !ok {
return
}
if err := selector.Set(*params.ProxyName); err != nil {
return
err = selector.Set(proxyName)
if err == nil {
log.Infoln("[Selector] %s selected %s", groupName, proxyName)
}
}()
return true
}
//export getTraffic
@@ -439,4 +440,10 @@ func init() {
Data: c,
})
}
executor.DefaultProxyProviderLoadedHook = func(providerName string) {
bridge.SendMessage(bridge.Message{
Type: bridge.Loaded,
Data: providerName,
})
}
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"sync"
"sync/atomic"
"syscall"
"time"
)
@@ -18,6 +19,21 @@ import (
var tunLock sync.Mutex
var tun *t.Tun
type FdMap struct {
m sync.Map
}
func (cm *FdMap) Store(key int64) {
cm.m.Store(key, struct{}{})
}
func (cm *FdMap) Load(key int64) bool {
_, ok := cm.m.Load(key)
return ok
}
var fdMap FdMap
//export startTUN
func startTUN(fd C.int) {
go func() {
@@ -63,15 +79,45 @@ func stopTun() {
var errBlocked = errors.New("blocked")
//export setFdMap
func setFdMap(fd C.long) {
fdInt := int64(fd)
go func() {
fdMap.Store(fdInt)
}()
}
var fdCounter int64 = 0
func init() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
if tun != nil {
tun.MarkSocket(int(fd))
time.Sleep(time.Millisecond * 100)
if tun == nil {
return
}
fdInt := int64(fd)
timeout := time.After(100 * time.Millisecond)
id := atomic.AddInt64(&fdCounter, 1)
tun.MarkSocket(t.Fd{
Id: id,
Value: fdInt,
})
for {
select {
case <-timeout:
return
default:
exists := fdMap.Load(id)
if exists {
return
}
time.Sleep(10 * time.Millisecond)
}
}
})
}

View File

@@ -19,7 +19,6 @@ import (
"io"
"net"
"os"
"strconv"
"time"
)
@@ -187,7 +186,12 @@ func Start(fd int, gateway, portal, dns string) (io.Closer, error) {
return stack, nil
}
func (t *Tun) MarkSocket(fd int) {
type Fd struct {
Id int64 `json:"id"`
Value int64 `json:"value"`
}
func (t *Tun) MarkSocket(fd Fd) {
_ = t.Limit.Acquire(context.Background(), 1)
defer t.Limit.Release(1)
@@ -197,7 +201,7 @@ func (t *Tun) MarkSocket(fd int) {
message := &bridge.Message{
Type: bridge.Tun,
Data: strconv.Itoa(fd),
Data: fd,
}
bridge.SendMessage(*message)

View File

@@ -115,7 +115,6 @@ class ApplicationState extends State<Application> {
lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateSystemColorSchemes(systemColorSchemes);
});

View File

@@ -117,6 +117,7 @@ class ClashCore {
.map(
(name) => proxies[name],
)
.where((proxy) => proxy != null)
.toList();
return group;
}).toList();
@@ -164,12 +165,11 @@ class ClashCore {
return completer.future;
}
bool changeProxy(ChangeProxyParams changeProxyParams) {
changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
final isInit = clashFFI.changeProxy(paramsChar) == 1;
clashFFI.changeProxy(paramsChar);
malloc.free(paramsChar);
return isInit;
}
Future<Delay> getDelay(String proxyName) {
@@ -185,7 +185,8 @@ class ClashCore {
receiver.close();
}
});
final delayParamsChar = json.encode(delayParams).toNativeUtf8().cast<Char>();
final delayParamsChar =
json.encode(delayParams).toNativeUtf8().cast<Char>();
clashFFI.asyncTestDelay(
delayParamsChar,
receiver.sendPort.nativePort,
@@ -254,11 +255,16 @@ class ClashCore {
}
void setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar = json.encode(processMapItem).toNativeUtf8().cast<Char>();
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
void setFdMap(int fd) {
clashFFI.setFdMap(fd);
}
// DateTime? getRunTime() {
// final runTimeString = clashFFI.getRunTime().cast<Utf8>().toDartString();
// if (runTimeString.isEmpty) return null;

View File

@@ -5248,7 +5248,7 @@ class ClashFFI {
late final _getProxies =
_getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
int changeProxy(
void changeProxy(
ffi.Pointer<ffi.Char> s,
) {
return _changeProxy(
@@ -5257,10 +5257,10 @@ class ClashFFI {
}
late final _changeProxyPtr =
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'changeProxy');
late final _changeProxy =
_changeProxyPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getTraffic() {
return _getTraffic();
@@ -5484,6 +5484,18 @@ class ClashFFI {
late final _stopTunPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap(
int fd,
) {
return _setFdMap(
fd,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
}
typedef va_list = ffi.Pointer<ffi.Char>;

View File

@@ -10,7 +10,7 @@ import 'core.dart';
abstract mixin class ClashMessageListener {
void onLog(Log log) {}
void onTun(String fd) {}
void onTun(Fd fd) {}
void onDelay(Delay delay) {}
@@ -21,6 +21,8 @@ abstract mixin class ClashMessageListener {
void onNow(Now now) {}
void onRun(String runTime) {}
void onLoaded(String groupName) {}
}
class ClashMessage {
@@ -39,7 +41,7 @@ class ClashMessage {
listener.onLog(Log.fromJson(m.data));
break;
case MessageType.tun:
listener.onTun(m.data);
listener.onTun(Fd.fromJson(m.data));
break;
case MessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
@@ -56,6 +58,9 @@ class ClashMessage {
case MessageType.run:
listener.onRun(m.data);
break;
case MessageType.loaded:
listener.onLoaded(m.data);
break;
}
}
});

View File

@@ -22,4 +22,6 @@ export 'string.dart';
export 'app_localizations.dart';
export 'function.dart';
export 'package.dart';
export 'measure.dart';
export 'measure.dart';
export 'service.dart';
export 'iterable.dart';

View File

@@ -24,6 +24,7 @@ const maxMobileWidth = 600;
const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204";
final filter = ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,

13
lib/common/iterable.dart Normal file
View File

@@ -0,0 +1,13 @@
extension IterableExt<T> on Iterable<T> {
Iterable<T> separated(T separator) sync* {
final iterator = this.iterator;
if (!iterator.moveNext()) return;
yield iterator.current;
while (iterator.moveNext()) {
yield separator;
yield iterator.current;
}
}
}

View File

@@ -1,22 +1,13 @@
import 'dart:async';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
class AppPackage{
import 'common.dart';
static AppPackage? _instance;
Completer<PackageInfo> packageInfoCompleter = Completer();
AppPackage._internal() {
PackageInfo.fromPlatform().then(
(value) => packageInfoCompleter.complete(value),
);
}
factory AppPackage() {
_instance ??= AppPackage._internal();
return _instance!;
}
extension PackageInfoExtension on PackageInfo {
String get ua => [
"$appName/v$version",
"clash-verge/v1.6.6",
"Platform/${Platform.operatingSystem}",
].join(" ");
}
final appPackage = AppPackage();

View File

@@ -1,29 +1,22 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart';
class Picker {
Future<PlatformFile?> pickerConfigFile() async {
FilePickerResult? filePickerResult;
if (Platform.isAndroid) {
filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
allowMultiple: false,
);
} else {
filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
type: FileType.custom,
allowedExtensions: ['yaml', 'txt', 'conf'],
);
}
final file = filePickerResult?.files.first;
if (file == null) {
return null;
}
return file;
final filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
allowMultiple: false,
);
return filePickerResult?.files.first;
}
Future<PlatformFile?> pickerGeoDataFile() async {
final filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
allowMultiple: false,
);
return filePickerResult?.files.first;
}
Future<String?> pickerConfigQRCode() async {

View File

@@ -12,17 +12,18 @@ class Request {
bool _isStart = false;
Request() {
_dio = Dio(
BaseOptions(
headers: {"User-Agent": coreName},
_dio = Dio();
_dio.options = BaseOptions(
headers: {"User-Agent": globalState.appController.clashConfig.globalUa},
);
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
_syncProxy();
return handler.next(options); // 继续请求
},
),
);
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
_syncProxy();
return handler.next(options); // 继续请求
},
));
}
_syncProxy() {
@@ -68,8 +69,7 @@ class Request {
if (response.statusCode != 200) return null;
final data = response.data as Map<String, dynamic>;
final remoteVersion = data['tag_name'];
final packageInfo = await appPackage.packageInfoCompleter.future;
final version = packageInfo.version;
final version = globalState.packageInfo.version;
final hasUpdate =
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
if (!hasUpdate) return null;

110
lib/common/service.dart Normal file
View File

@@ -0,0 +1,110 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
typedef CreateServiceNative = IntPtr Function(
IntPtr hSCManager,
Pointer<Utf16> lpServiceName,
Pointer<Utf16> lpDisplayName,
Uint32 dwDesiredAccess,
Uint32 dwServiceType,
Uint32 dwStartType,
Uint32 dwErrorControl,
Pointer<Utf16> lpBinaryPathName,
Pointer<Utf16> lpLoadOrderGroup,
Pointer<Uint32> lpdwTagId,
Pointer<Utf16> lpDependencies,
Pointer<Utf16> lpServiceStartName,
Pointer<Utf16> lpPassword,
);
typedef CreateServiceDart = int Function(
int hSCManager,
Pointer<Utf16> lpServiceName,
Pointer<Utf16> lpDisplayName,
int dwDesiredAccess,
int dwServiceType,
int dwStartType,
int dwErrorControl,
Pointer<Utf16> lpBinaryPathName,
Pointer<Utf16> lpLoadOrderGroup,
Pointer<Uint32> lpdwTagId,
Pointer<Utf16> lpDependencies,
Pointer<Utf16> lpServiceStartName,
Pointer<Utf16> lpPassword,
);
const _SERVICE_ALL_ACCESS = 0xF003F;
const _SERVICE_WIN32_OWN_PROCESS = 0x00000010;
const _SERVICE_AUTO_START = 0x00000002;
const _SERVICE_ERROR_NORMAL = 0x00000001;
typedef GetLastErrorNative = Uint32 Function();
typedef GetLastErrorDart = int Function();
class Service {
static Service? _instance;
late DynamicLibrary _advapi32;
Service._internal() {
_advapi32 = DynamicLibrary.open('advapi32.dll');
}
factory Service() {
_instance ??= Service._internal();
return _instance!;
}
Future<void> createService() async {
final int scManager = OpenSCManager(nullptr, nullptr, _SERVICE_ALL_ACCESS);
if (scManager == 0) return;
final serviceName = 'FlClash Service'.toNativeUtf16();
final displayName = 'FlClash Service'.toNativeUtf16();
final binaryPathName = "C:\\Application\\Clash.Verge_1.6.6_x64_portable\\resources\\clash-verge-service.exe".toNativeUtf16();
final createService =
_advapi32.lookupFunction<CreateServiceNative, CreateServiceDart>(
'CreateServiceW',
);
final getLastError = DynamicLibrary.open('kernel32.dll')
.lookupFunction<GetLastErrorNative, GetLastErrorDart>('GetLastError');
final serviceHandle = createService(
scManager,
serviceName,
displayName,
_SERVICE_ALL_ACCESS,
_SERVICE_WIN32_OWN_PROCESS,
_SERVICE_AUTO_START,
_SERVICE_ERROR_NORMAL,
binaryPathName,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
);
print("serviceHandle $serviceHandle");
final errorCode = GetLastError();
print('Error code: $errorCode');
final result = StartService(serviceHandle, 0, nullptr);
if (result == 0) {
print('Failed to start the service.');
} else {
print('Service started successfully.');
}
calloc.free(serviceName);
calloc.free(displayName);
calloc.free(binaryPathName);
}
}
final service = Platform.isWindows ? Service() : null;

View File

@@ -1,10 +1,14 @@
extension StringExtension on String {
bool get isUrl {
try {
Uri.parse(this);
return true;
} catch (e) {
return false;
}
return RegExp(
r'^(https?:\/\/)?'
r'((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|'
r'((\d{1,3}\.){3}\d{1,3}))'
r'(:\d+)?'
r'(\/[-a-z\d%_.~+]*)*'
r'(\?[;&a-z\d%_.~+=-]*)?'
r'(\#[-a-z\d_]*)?$',
caseSensitive: false,
).hasMatch(this);
}
}

View File

@@ -10,5 +10,5 @@ extension TextStyleExtension on TextStyle {
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
TextStyle get toMinus => copyWith(fontSize: fontSize! - 1);
TextStyle get toMinus => copyWith(fontSize: fontSize! - 2);
}

View File

@@ -17,6 +17,7 @@ class AppController {
late ClashConfig clashConfig;
late Measure measure;
late Function updateClashConfigDebounce;
late Function addCheckIpNumDebounce;
AppController(this.context) {
appState = context.read<AppState>();
@@ -25,6 +26,9 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
});
addCheckIpNumDebounce = debounce((){
appState.checkIpNum++;
});
measure = Measure.of(context);
}
@@ -71,14 +75,6 @@ class AppController {
);
}
changeProxy() {
globalState.changeProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
addProfile(Profile profile) async {
config.setProfile(profile);
if (config.currentProfileId != null) return;

View File

@@ -56,7 +56,16 @@ enum ProfileType { file, url }
enum ResultType { success, error }
enum MessageType { log, tun, delay, process, now, request, run }
enum MessageType {
log,
tun,
delay,
process,
now,
request,
run,
loaded,
}
enum FindProcessMode { always, off }

View File

@@ -1,7 +1,6 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutFragment extends StatelessWidget {
@@ -49,16 +48,9 @@ class AboutFragment extends StatelessWidget {
appName,
style: Theme.of(context).textTheme.headlineSmall,
),
FutureBuilder<PackageInfo>(
future: appPackage.packageInfoCompleter.future,
builder: (_, package) {
final version = package.data?.version;
if (version == null) return Container();
return Text(
version,
style: Theme.of(context).textTheme.labelLarge,
);
},
Text(
globalState.packageInfo.version,
style: Theme.of(context).textTheme.labelLarge,
)
],
)

View File

@@ -6,7 +6,6 @@ import 'package:fl_clash/models/dav.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/section.dart';
import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -34,7 +33,7 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
final res = await commonScaffoldState?.loadingRun<bool>(() async {
return await _client?.backup();
});
if(res != true) return;
if (res != true) return;
globalState.showMessage(
title: appLocalizations.recovery,
message: TextSpan(text: appLocalizations.backupSuccess),
@@ -46,7 +45,7 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
final res = await commonScaffoldState?.loadingRun<bool>(() async {
return await _client?.recovery(recoveryOption: recoveryOption);
});
if(res != true) return;
if (res != true) return;
globalState.showMessage(
title: appLocalizations.recovery,
message: TextSpan(text: appLocalizations.recoverySuccess),
@@ -69,26 +68,22 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
if (dav == null) {
return ListView(
children: [
Section(
ListHeader(
title: appLocalizations.account,
child: Builder(
builder: (_) {
return ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
subtitle: Text(appLocalizations.pleaseBindWebDAV),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
);
),
ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
subtitle: Text(appLocalizations.pleaseBindWebDAV),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
)
),
],
);
}
@@ -96,62 +91,60 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
final pingFuture = _client!.pingCompleter.future;
return ListView(
children: [
Section(
title: appLocalizations.account,
child: ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
ListHeader(title: appLocalizations.account),
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: pingFuture,
builder: (_, snapshot) {
return Center(
child: FadeBox(
key: const Key("fade_box_1"),
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: pingFuture,
builder: (_, snapshot) {
return Center(
child: FadeBox(
key: const Key("fade_box_1"),
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
),
);
},
),
],
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
@@ -161,22 +154,21 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
return FadeBox(
key: const Key("fade_box_2"),
child: snapshot.data == true
? Section(
title: appLocalizations.backupAndRecovery,
child: Column(
children: [
ListItem(
onTab: _backup,
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.backupDesc),
),
ListItem(
onTab: _handleRecovery,
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.recoveryDesc),
),
],
),
? Column(
children: [
ListHeader(
title: appLocalizations.backupAndRecovery),
ListItem(
onTab: _backup,
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.backupDesc),
),
ListItem(
onTab: _handleRecovery,
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.recoveryDesc),
),
],
)
: Container(),
);
@@ -228,7 +220,6 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
Navigator.pop(context);
}
@override
void dispose() {
super.dispose();

View File

@@ -39,81 +39,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
}
}
Widget _buildAppSection() {
final items = [
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined),
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet),
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.systemProxy = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
return ListItem.switchItem(
leading: const Icon(Icons.expand_outlined),
title: Text(appLocalizations.compatible),
subtitle: Text(appLocalizations.compatibleDesc),
delegate: SwitchDelegate(
value: isCompatible,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCompatible = value;
await appController.updateClashConfig(isPatch: false);
await appController.updateGroups();
appController.changeProxy();
},
),
);
},
),
];
return Section(
title: appLocalizations.app,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
);
}
_showLogLevelDialog(LogLevel value) {
globalState.showCommonDialog(
child: AlertDialog(
@@ -150,238 +75,364 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
}
Widget _buildGeneralSection() {
final items = [
Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
onTab: () {
_showLogLevelDialog(value);
},
);
},
),
Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
return ListItem(
onTab: () {
_modifyMixedPort(mixedPort);
},
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text(appLocalizations.proxyPortDesc),
trailing: FilledButton.tonal(
onPressed: () {
_modifyMixedPort(mixedPort);
},
child: Text(
"$mixedPort",
),
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.allowLan,
builder: (_, allowLan, __) {
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: Text(appLocalizations.allowLan),
subtitle: Text(appLocalizations.allowLanDesc),
delegate: SwitchDelegate(
value: allowLan,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.allowLan = value;
globalState.appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.unifiedDelay,
builder: (_, unifiedDelay, __) {
return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: unifiedDelay,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) {
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
builder: (_, tcpConcurrent, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.geodataLoader == geodataLoaderMemconservative,
builder: (_, memconservative, __) {
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: memconservative,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative
: geodataLoaderStandard;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
builder: (_, hasExternalController, __) {
return ListItem.switchItem(
leading: const Icon(Icons.api_outlined),
title: Text(appLocalizations.externalController),
subtitle: Text(appLocalizations.externalControllerDesc),
delegate: SwitchDelegate(
value: hasExternalController,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
appController.updateClashConfigDebounce();
},
),
);
},
),
_showUaDialog(String? value) {
const uas = [
null,
"clash-verge/v1.6.6",
"ClashforWindows/0.19.23",
];
return Section(
title: appLocalizations.general,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
globalState.showCommonDialog(
child: AlertDialog(
title: const Text("UA"),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
for (final ua in uas)
ListItem.radio(
delegate: RadioDelegate<String?>(
value: ua,
groupValue: value,
onChanged: (String? value) {
final appController = globalState.appController;
appController.clashConfig.globalRealUa = value;
appController.updateClashConfigDebounce();
Navigator.of(context).pop();
},
),
title: Text(ua ?? appLocalizations.defaultText),
)
],
),
),
),
);
}
Widget _buildMoreSection() {
final items = [
if (false)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
_modifyTestUrl(String testUrl) async {
final newTestUrl = await globalState.showCommonDialog<String>(
child: TestUrlFormDialog(
testUrl: testUrl,
),
);
if (newTestUrl != null && newTestUrl != testUrl && mounted) {
try {
if (!newTestUrl.isUrl) {
throw "Invalid url";
}
globalState.appController.config.testUrl = newTestUrl;
globalState.appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
}
}
}
List<Widget> _buildAppSection() {
return generateSection(
title: appLocalizations.app,
items: [
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined),
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet),
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.systemProxy = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
return ListItem.switchItem(
leading: const Icon(
Icons.important_devices_outlined
),
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
leading: const Icon(Icons.expand_outlined),
title: Text(appLocalizations.compatible),
subtitle: Text(appLocalizations.compatibleDesc),
delegate: SwitchDelegate(
value: tunEnable,
value: isCompatible,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCompatible = value;
await appController.applyProfile();
},
),
);
},
),
],
);
}
List<Widget> _buildGeneralSection() {
return generateSection(
title: appLocalizations.general,
items: [
Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
onTab: () {
_showLogLevelDialog(value);
},
);
},
),
Selector<ClashConfig, String?>(
selector: (_, clashConfig) => clashConfig.globalRealUa,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(value ?? appLocalizations.defaultText),
onTab: () {
_showUaDialog(value);
},
);
},
),
Selector<Config, String>(
selector: (_, config) => config.testUrl,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(value),
onTab: () {
_modifyTestUrl(value);
},
);
},
),
Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
return ListItem(
onTab: () {
_modifyMixedPort(mixedPort);
},
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text(appLocalizations.proxyPortDesc),
trailing: FilledButton.tonal(
onPressed: () {
_modifyMixedPort(mixedPort);
},
child: Text(
"$mixedPort",
),
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.allowLan,
builder: (_, allowLan, __) {
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: Text(appLocalizations.allowLan),
subtitle: Text(appLocalizations.allowLanDesc),
delegate: SwitchDelegate(
value: allowLan,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value);
clashConfig.allowLan = value;
globalState.appController.updateClashConfigDebounce();
},
),
);
},
),
];
if(items.isEmpty) return Container();
return Section(
title: appLocalizations.general,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.unifiedDelay,
builder: (_, unifiedDelay, __) {
return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: unifiedDelay,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) {
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
builder: (_, tcpConcurrent, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.geodataLoader == geodataLoaderMemconservative,
builder: (_, memconservative, __) {
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: memconservative,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative
: geodataLoaderStandard;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.externalController.isNotEmpty,
builder: (_, hasExternalController, __) {
return ListItem.switchItem(
leading: const Icon(Icons.api_outlined),
title: Text(appLocalizations.externalController),
subtitle: Text(appLocalizations.externalControllerDesc),
delegate: SwitchDelegate(
value: hasExternalController,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
appController.updateClashConfigDebounce();
},
),
);
},
),
],
);
}
List<Widget> _buildMoreSection() {
return generateSection(
title: appLocalizations.more,
items: [
if (system.isDesktop)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
return ListItem.switchItem(
leading: const Icon(Icons.important_devices_outlined),
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: tunEnable,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value);
globalState.appController.updateClashConfigDebounce();
},
),
);
},
),
],
);
}
@override
Widget build(BuildContext context) {
List<Widget> items = [
_buildAppSection(),
_buildGeneralSection(),
_buildMoreSection(),
..._buildAppSection(),
..._buildGeneralSection(),
..._buildMoreSection(),
];
return ListView.builder(
padding: const EdgeInsets.only(bottom: 32),
@@ -414,7 +465,7 @@ class _MixedPortFormDialogState extends State<MixedPortFormDialog> {
portController = TextEditingController(text: "${widget.mixedPort}");
}
_handleAddProfileFormURL() async {
_handleUpdate() async {
final port = portController.value.text;
if (port.isEmpty) return;
Navigator.of(context).pop<String>(port);
@@ -440,7 +491,64 @@ class _MixedPortFormDialogState extends State<MixedPortFormDialog> {
),
actions: [
TextButton(
onPressed: _handleAddProfileFormURL,
onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
],
);
}
}
class TestUrlFormDialog extends StatefulWidget {
final String testUrl;
const TestUrlFormDialog({
super.key,
required this.testUrl,
});
@override
State<TestUrlFormDialog> createState() => _TestUrlFormDialogState();
}
class _TestUrlFormDialogState extends State<TestUrlFormDialog> {
late TextEditingController testUrlController;
@override
void initState() {
super.initState();
testUrlController = TextEditingController(text: widget.testUrl);
}
_handleUpdate() async {
final testUrl = testUrlController.value.text;
if (testUrl.isEmpty) return;
Navigator.of(context).pop<String>(testUrl);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.testUrl),
content: SizedBox(
width: 300,
child: Wrap(
runSpacing: 16,
children: [
TextField(
maxLines: 5,
minLines: 1,
controller: testUrlController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
],
),
),
actions: [
TextButton(
onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
],

View File

@@ -139,8 +139,8 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(
@@ -219,6 +219,10 @@ class ConnectionItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid
? Container(
@@ -249,17 +253,17 @@ class ConnectionItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 12,
height: 8,
),
Wrap(
runSpacing: 8,
spacing: 8,
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
@@ -271,9 +275,6 @@ class ConnectionItem extends StatelessWidget {
),
],
),
const SizedBox(
height: 12,
),
],
),
trailing: IconButton(
@@ -394,8 +395,8 @@ class ConnectionsSearchDelegate extends SearchDelegate {
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(

View File

@@ -53,7 +53,7 @@ class _IntranetIPState extends State<IntranetIP> {
},
child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.appController.measure.titleLargeHeight + 24 - 1,
height: globalState.appController.measure.titleLargeHeight + 24 - 2,
child: ValueListenableBuilder(
valueListenable: ipNotifier,
builder: (_, value, __) {

View File

@@ -52,6 +52,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
isInit: appState.isInit,
selectedMap: appState.selectedMap,
isStart: appState.isStart,
checkIpNum: appState.checkIpNum,
);
},
builder: (_, state, __) {
@@ -137,7 +138,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
Container(
height: globalState.appController.measure.titleLargeHeight +
24 -
1,
2,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox(

View File

@@ -13,18 +13,9 @@ class OutboundMode extends StatelessWidget {
_changeMode(BuildContext context, Mode? value) async {
final appController = globalState.appController;
final clashConfig = appController.clashConfig;
final config = appController.config;
if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value;
await appController.updateClashConfig();
if (!config.isCompatible) {
final proxySelected = config.currentSelectedMap[GroupName.Proxy.name];
final globalSelected = config.currentSelectedMap[GroupName.GLOBAL.name];
if (proxySelected != null && globalSelected == null) {
config.updateCurrentSelectedMap(GroupName.GLOBAL.name, proxySelected);
}
}
appController.changeProxy();
}
@override
@@ -64,11 +55,8 @@ class OutboundMode extends StatelessWidget {
),
title: Text(
Intl.message(item.name),
style: Theme
.of(context)
.textTheme
.titleMedium
?.toSoftBold,
style:
Theme.of(context).textTheme.titleMedium?.toSoftBold,
),
),
],

View File

@@ -269,8 +269,8 @@ class LogsSearchDelegate extends SearchDelegate {
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(
@@ -328,26 +328,23 @@ class _LogItemState extends State<LogItem> {
@override
Widget build(BuildContext context) {
final log = widget.log;
return ListTile(
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: SelectableText(log.payload ?? ''),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
top: 8,
),
child: SelectableText(
"${log.dateTime}",
style: context.textTheme.bodySmall
?.copyWith(color: context.colorScheme.primary),
),
SelectableText(
"${log.dateTime}",
style: context.textTheme.bodySmall
?.copyWith(color: context.colorScheme.primary),
),
const SizedBox(height: 8,),
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(
vertical: 8,
),
child: CommonChip(
onPressed: () {
if (widget.onClick == null) return;

View File

@@ -28,6 +28,7 @@ class ProfilesFragment extends StatefulWidget {
class _ProfilesFragmentState extends State<ProfilesFragment> {
final hasPadding = ValueNotifier<bool>(false);
Function? applyConfigDebounce;
List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = [];
@@ -86,12 +87,30 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
hasPadding.dispose();
}
_changeProfile(String? id) async {
final appController = globalState.appController;
final config = appController.config;
if (id == config.currentProfileId) return;
config.currentProfileId = id;
applyConfigDebounce ??= debounce<Function()>(() async {
await appController.applyProfile();
appController.appState.delayMap = {};
appController.saveConfigPreferences();
});
applyConfigDebounce!();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
@@ -118,33 +137,30 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
.map((profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
.toList();
final columns = _getColumns(state.viewMode);
final isMobile = state.viewMode == ViewMode.mobile;
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
WidgetsBinding.instance.addPostFrameCallback((_) {
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
});
WidgetsBinding.instance.addPostFrameCallback(
(_) {
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
},
);
return true;
},
child: ValueListenableBuilder(
valueListenable: hasPadding,
builder: (_, hasPadding, __) {
return SingleChildScrollView(
padding: !isMobile
? EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + (hasPadding ? 56 : 0),
)
: EdgeInsets.only(
bottom: 0 + (hasPadding ? 56 : 0),
),
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + (hasPadding ? 56 : 0),
),
child: Grid(
mainAxisSpacing: isMobile ? 8 : 16,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
@@ -154,8 +170,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
key: profileItemKeys[i],
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged:
globalState.appController.changeProfile,
onChanged: _changeProfile,
),
),
],
@@ -206,6 +221,8 @@ class _ProfileItemState extends State<ProfileItem> {
isUpdating.value = false;
if (!isSingle) {
return e.toString();
} else {
rethrow;
}
}
isUpdating.value = false;
@@ -393,16 +410,7 @@ class _ProfileItemState extends State<ProfileItem> {
final profile = widget.profile;
final groupValue = widget.groupValue;
final onChanged = widget.onChanged;
return Selector<AppState, ViewMode>(
selector: (_, appState) => appState.viewMode,
builder: (_, viewMode, child) {
if (viewMode == ViewMode.mobile) {
return child!;
}
return CommonCard(
child: child!,
);
},
return CommonCard(
child: ListItem.radio(
key: Key(profile.id),
horizontalTitleGap: 16,

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
@@ -280,7 +279,6 @@ class ProxyGroupView extends StatefulWidget {
class _ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false;
final isBoundaryNotifier = ValueNotifier<bool>(false);
final scrollController = ScrollController();
var isEnd = false;
@@ -374,53 +372,6 @@ class _ProxyGroupViewState extends State<ProxyGroupView> {
);
}
Widget _androidExpansionHandle(Widget child) {
// return NotificationListener<ScrollNotification>(
// onNotification: (ScrollNotification notification) {
// if (notification is ScrollEndNotification) {
// if (notification.metrics.atEdge) {
// isEnd = notification.metrics.pixels ==
// notification.metrics.maxScrollExtent;
// isBoundaryNotifier.value = true;
// }
// }
// return false;
// },
// child: Listener(
// onPointerMove: (details) {
// double yOffset = details.delta.dy;
// final isEnd = scrollController.position.maxScrollExtent == scrollController.position.pixels;
// final isTop = scrollController.position.minScrollExtent == scrollController.position.pixels;
// if(isEnd || isTop){
// isBoundaryNotifier.value = true;
// } else if (yOffset > 0 && scrollController.position.maxScrollExtent == scrollController.position.pixels) {
// isBoundaryNotifier.value = false;
// } else if (yOffset < 0 && !isEnd) {
// isBoundaryNotifier.value = false;
// }
// },
// child: child,
// ),
// );
return Listener(
onPointerMove: (details) {
double yOffset = details.delta.dy;
final isEnd = scrollController.position.maxScrollExtent ==
scrollController.position.pixels;
final isTop = scrollController.position.minScrollExtent ==
scrollController.position.pixels;
if (isEnd && yOffset < 0) {
isBoundaryNotifier.value = true;
} else if (isTop && yOffset > 0) {
isBoundaryNotifier.value = true;
} else {
isBoundaryNotifier.value = false;
}
},
child: child,
);
}
Widget _buildExpansionGroupView({
required List<Proxy> proxies,
required int columns,
@@ -544,75 +495,32 @@ class _ProxyGroupViewState extends State<ProxyGroupView> {
children: [
SizedBox(
height: height,
child: Platform.isAndroid
? _androidExpansionHandle(
ValueListenableBuilder(
valueListenable: isBoundaryNotifier,
builder: (_, isBoundary, child) {
return Scrollbar(
thickness: 6,
interactive: true,
radius: const Radius.circular(6),
child: GridView.builder(
key: widget.key,
controller: scrollController,
physics: isBoundary || !hasScrollable
? const NeverScrollableScrollPhysics()
: const AlwaysScrollableScrollPhysics(),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return _currentProxyNameBuilder(
builder: (value) {
return ProxyCard(
style: CommonCardType.filled,
type: proxyCardType,
isSelected: value == proxy.name,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
});
},
),
);
},
),
)
: GridView.builder(
key: widget.key,
controller: scrollController,
physics: !hasScrollable
? const NeverScrollableScrollPhysics()
: const AlwaysScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return _currentProxyNameBuilder(builder: (value) {
return ProxyCard(
style: CommonCardType.filled,
type: proxyCardType,
isSelected: value == proxy.name,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
});
},
),
child: GridView.builder(
key: widget.key,
controller: scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return _currentProxyNameBuilder(
builder: (value) {
return ProxyCard(
style: CommonCardType.filled,
type: proxyCardType,
isSelected: value == proxy.name,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
);
},
),
),
],
),
@@ -624,7 +532,6 @@ class _ProxyGroupViewState extends State<ProxyGroupView> {
@override
void dispose() {
super.dispose();
isBoundaryNotifier.dispose();
scrollController.dispose();
}
@@ -831,7 +738,12 @@ class ProxyCard extends StatelessWidget {
groupName,
proxy.name,
);
appController.changeProxy();
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxy.name,
),
);
}
@override

View File

@@ -137,8 +137,8 @@ class _RequestsFragmentState extends State<RequestsFragment> {
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(
@@ -214,6 +214,10 @@ class RequestItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid
? Container(
@@ -244,17 +248,17 @@ class RequestItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 12,
height: 8,
),
Wrap(
runSpacing: 8,
spacing: 8,
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
@@ -266,9 +270,6 @@ class RequestItem extends StatelessWidget {
),
],
),
const SizedBox(
height: 12,
),
],
),
);
@@ -375,8 +376,8 @@ class RequestsSearchDelegate extends SearchDelegate {
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(

View File

@@ -2,7 +2,8 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/ffi.dart';
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:path/path.dart' hide context;
@@ -18,6 +19,17 @@ class GeoItem {
});
}
@immutable
class FileInfo {
final String size;
final DateTime lastModified;
const FileInfo({
required this.size,
required this.lastModified,
});
}
class Resources extends StatefulWidget {
const Resources({super.key});
@@ -26,147 +38,356 @@ class Resources extends StatefulWidget {
}
class _ResourcesState extends State<Resources> {
_updateExternalProvider(
String providerName,
String providerType,
) async {
final commonScaffoldState = context.commonScaffoldState;
await commonScaffoldState?.loadingRun(() async {
final message = await clashCore.updateExternalProvider(
providerName: providerName,
providerType: providerType,
);
if (message.isNotEmpty) throw message;
List<ExternalProvider> externalProviders = [];
List<GlobalObjectKey<_ProviderItemState>> providerItemKeys = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_syncExternalProviders();
});
}
_syncExternalProviders() async {
externalProviders = await clashCore.getExternalProviders();
setState(() {});
}
Future<DateTime> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath();
return await File(join(homePath, fileName)).lastModified();
}
Widget _buildExternalProviderSection() {
return FutureBuilder<List<ExternalProvider>>(
future: () async {
await Future.delayed(const Duration(milliseconds: 200));
return await clashCore.getExternalProviders();
}(),
builder: (_, snapshot) {
return Center(
child: FadeBox(
key: const Key("external_providers"),
child: snapshot.data == null || snapshot.data!.isEmpty
? Container()
: Section(
title: appLocalizations.externalResources,
child: Column(
children: [
for (final externalProvider in snapshot.data!)
ListItem(
title: Text(externalProvider.name),
subtitle: Text(
"${externalProvider.type} (${externalProvider.vehicleType})",
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
externalProvider.updateAt.lastUpdateTimeDesc,
style: context.textTheme.bodyMedium,
),
const Padding(
padding: EdgeInsets.only(left: 12,right: 4),
child: VerticalDivider(
endIndent: 6,
width: 4,
indent: 6,
),
),
externalProvider.vehicleType == "HTTP"
? IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
_updateExternalProvider(
externalProvider.name,
externalProvider.type,
);
},
)
: Container(),
],
),
)
],
),
),
),
);
},
_updateProviders() async {
print(providerItemKeys);
final updateProviders = providerItemKeys.map<Future>(
(key) async => await key.currentState?.updateProvider(false),
);
await Future.wait(updateProviders);
_syncExternalProviders();
}
Widget _buildGeoDataSection() {
List<Widget> _buildExternalProviderSection() {
List<GlobalObjectKey<_ProviderItemState>> keys = [];
final res = generateSection(
title: appLocalizations.externalResources,
actions: [
IconButton(
onPressed: () {
_updateProviders();
},
icon: const Icon(
Icons.sync,
),
)
],
items: externalProviders.map(
(externalProvider) {
final key =
GlobalObjectKey<_ProviderItemState>(externalProvider.name);
keys.add(key);
return ProviderItem(
key: key,
provider: externalProvider,
onUpdated: () {
_syncExternalProviders();
},
);
},
),
);
providerItemKeys = keys;
return res;
}
List<Widget> _buildGeoDataSection() {
const geoItems = <GeoItem>[
GeoItem(label: "GeoIp", fileName: mmdbFileName),
GeoItem(label: "GeoSite", fileName: geoSiteFileName),
GeoItem(label: "ASN", fileName: asnFileName),
];
return Section(
return generateSection(
title: appLocalizations.geoData,
child: Column(
children: [
for (final geoItem in geoItems)
ListItem(
title: Text(geoItem.label),
subtitle: FutureBuilder<DateTime>(
future: () async {
await Future.delayed(const Duration(milliseconds: 200));
return await _getGeoFileLastModified(geoItem.fileName);
}(),
builder: (_, snapshot) {
return Container(
alignment: Alignment.centerLeft,
height: 24,
child: FadeBox(
key: Key("fade_box_${geoItem.label}"),
child: snapshot.data == null
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
snapshot.data!.lastUpdateTimeDesc,
),
),
);
},
),
trailing: IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
_updateExternalProvider(
geoItem.fileName,
geoItem.label,
);
},
),
),
],
items: geoItems.map(
(geoItem) => GeoDataListItem(
geoItem: geoItem,
),
),
);
}
@override
Widget build(BuildContext context) {
return ListView(
children: [
_buildGeoDataSection(),
_buildExternalProviderSection(),
return generateListView(
[
..._buildGeoDataSection(),
..._buildExternalProviderSection(),
],
);
}
}
class GeoDataListItem extends StatefulWidget {
final GeoItem geoItem;
const GeoDataListItem({
super.key,
required this.geoItem,
});
@override
State<GeoDataListItem> createState() => _GeoDataListItemState();
}
class _GeoDataListItemState extends State<GeoDataListItem> {
final isUpdating = ValueNotifier<bool>(false);
GeoItem get geoItem => widget.geoItem;
Future<FileInfo> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath();
final file = File(join(homePath, fileName));
final lastModified = await file.lastModified();
final size = await file.length();
return FileInfo(
size: TrafficValue(value: size).show,
lastModified: lastModified,
);
}
// _uploadGeoFile(String fileName) async {
// final res = await picker.pickerGeoDataFile();
// if (res == null || res.bytes == null) return;
// final homePath = await appPath.getHomeDirPath();
// final file = File(join(homePath, fileName));
// await file.writeAsBytes(
// res.bytes!,
// flush: true,
// );
// setState(() {});
// }
String _buildFileInfoDesc(FileInfo fileInfo) {
return "${fileInfo.size} · ${fileInfo.lastModified.lastUpdateTimeDesc}";
}
Widget _buildSubtitle() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
FutureBuilder<FileInfo>(
future: _getGeoFileLastModified(geoItem.fileName),
builder: (_, snapshot) {
return SizedBox(
height: 24,
child: FadeBox(
key: Key("fade_box_${geoItem.label}"),
child: snapshot.data == null
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
_buildFileInfoDesc(snapshot.data!),
),
),
);
},
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 12,
children: [
// CommonChip(
// avatar: const Icon(Icons.upload),
// label: "编辑",
// onPressed: () {
// _uploadGeoFile(geoItem.fileName);
// },
// ),
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: () {
_handleUpdateGeoDataItem();
},
),
],
),
],
);
}
_handleUpdateGeoDataItem() async {
await globalState.safeRun<void>(updateGeoDateItem);
setState(() {});
}
updateGeoDateItem() async {
isUpdating.value = true;
try {
final message = await clashCore.updateExternalProvider(
providerName: geoItem.fileName,
providerType: geoItem.label,
);
if (message.isNotEmpty) throw message;
} catch (e) {
isUpdating.value = false;
rethrow;
}
isUpdating.value = false;
return null;
}
@override
void dispose() {
super.dispose();
isUpdating.dispose();
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: Text(geoItem.label),
subtitle: _buildSubtitle(),
trailing: SizedBox(
height: 48,
width: 48,
child: ValueListenableBuilder(
valueListenable: isUpdating,
builder: (_, isUpdating, ___) {
return FadeBox(
child: isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: const SizedBox(),
);
},
),
),
);
}
}
class ProviderItem extends StatefulWidget {
final ExternalProvider provider;
final Function onUpdated;
const ProviderItem({
super.key,
required this.provider,
required this.onUpdated,
});
@override
State<ProviderItem> createState() => _ProviderItemState();
}
class _ProviderItemState extends State<ProviderItem> {
final isUpdating = ValueNotifier<bool>(false);
ExternalProvider get provider => widget.provider;
_handleUpdateProfile() async {
await globalState.safeRun<void>(updateProvider);
widget.onUpdated();
}
updateProvider([isSingle = true]) async {
if (provider.vehicleType != "HTTP") return;
isUpdating.value = true;
try {
final message = await clashCore.updateExternalProvider(
providerName: provider.name,
providerType: provider.type,
);
if (message.isNotEmpty) throw message;
} catch (e) {
isUpdating.value = false;
if (!isSingle) {
return e.toString();
} else {
rethrow;
}
}
isUpdating.value = false;
return null;
}
String _buildProviderDesc() {
return "${provider.type} (${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}";
}
@override
void dispose() {
super.dispose();
isUpdating.dispose();
}
Widget _buildSubtitle() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
Text(
_buildProviderDesc(),
),
if (provider.vehicleType == "HTTP") ...[
const SizedBox(
height: 8,
),
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: () {
_handleUpdateProfile();
},
),
],
],
);
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: Text(provider.name),
subtitle: _buildSubtitle(),
trailing: SizedBox(
height: 48,
width: 48,
child: ValueListenableBuilder(
valueListenable: isUpdating,
builder: (_, isUpdating, ___) {
return FadeBox(
child: isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: const SizedBox(),
);
},
),
),
);
}
}

View File

@@ -57,138 +57,120 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
return Intl.message(locale.toString());
}
Widget _getOtherList() {
List<Widget> items = [
ListItem.open(
leading: const Icon(Icons.info),
title: Text(appLocalizations.about),
delegate: OpenDelegate(
title: appLocalizations.about,
widget: const AboutFragment(),
List<Widget> _getOtherList() {
return generateSection(
title: appLocalizations.other,
items: [
ListItem.open(
leading: const Icon(Icons.info),
title: Text(appLocalizations.about),
delegate: OpenDelegate(
title: appLocalizations.about,
widget: const AboutFragment(),
),
),
),
];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final item in items) ...[
item,
if (item != items.last)
const Divider(
height: 0,
),
]
],
);
}
Widget _getSettingList() {
List<Widget> items = [
Selector<Config, String?>(
selector: (_, config) => config.locale,
builder: (_, localeString, __) {
final subTitle = localeString ?? appLocalizations.defaultText;
final currentLocale = other.getLocaleForString(localeString);
return ListTile(
leading: const Icon(Icons.language_outlined),
title: Text(appLocalizations.language),
subtitle: Text(Intl.message(subTitle)),
onTap: () {
globalState.showCommonDialog(
child: AlertDialog(
title: Text(appLocalizations.language),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
for (final locale in [
null,
...AppLocalizations.delegate.supportedLocales
])
ListItem.radio(
delegate: RadioDelegate<Locale?>(
value: locale,
groupValue: currentLocale,
onChanged: (Locale? value) {
final config = context.read<Config>();
config.locale = value?.toString();
Navigator.of(context).pop();
},
),
title: Text(_getLocaleString(locale)),
)
],
List<Widget> _getSettingList() {
return generateSection(
title: appLocalizations.settings,
items: [
Selector<Config, String?>(
selector: (_, config) => config.locale,
builder: (_, localeString, __) {
final subTitle = localeString ?? appLocalizations.defaultText;
final currentLocale = other.getLocaleForString(localeString);
return ListTile(
leading: const Icon(Icons.language_outlined),
title: Text(appLocalizations.language),
subtitle: Text(Intl.message(subTitle)),
onTap: () {
globalState.showCommonDialog(
child: AlertDialog(
title: Text(appLocalizations.language),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
for (final locale in [
null,
...AppLocalizations.delegate.supportedLocales
])
ListItem.radio(
delegate: RadioDelegate<Locale?>(
value: locale,
groupValue: currentLocale,
onChanged: (Locale? value) {
final config = context.read<Config>();
config.locale = value?.toString();
Navigator.of(context).pop();
},
),
title: Text(_getLocaleString(locale)),
)
],
),
),
),
),
);
},
);
},
),
ListItem.open(
leading: const Icon(Icons.style),
title: Text(appLocalizations.theme),
subtitle: Text(appLocalizations.themeDesc),
delegate: OpenDelegate(
title: appLocalizations.theme,
widget: const ThemeFragment(),
extendPageWidth: 360,
);
},
);
},
),
),
ListItem.open(
leading: const Icon(Icons.cloud_sync),
title: Text(appLocalizations.backupAndRecovery),
subtitle: Text(appLocalizations.backupAndRecoveryDesc),
delegate: OpenDelegate(
title: appLocalizations.backupAndRecovery,
widget: const BackupAndRecovery(),
),
),
if (Platform.isAndroid)
ListItem.open(
leading: const Icon(Icons.view_list),
title: Text(appLocalizations.accessControl),
subtitle: Text(appLocalizations.accessControlDesc),
leading: const Icon(Icons.style),
title: Text(appLocalizations.theme),
subtitle: Text(appLocalizations.themeDesc),
delegate: OpenDelegate(
title: appLocalizations.appAccessControl,
widget: const AccessFragment(),
title: appLocalizations.theme,
widget: const ThemeFragment(),
extendPageWidth: 360,
),
),
ListItem.open(
leading: const Icon(Icons.edit),
title: Text(appLocalizations.override),
subtitle: Text(appLocalizations.overrideDesc),
delegate: OpenDelegate(
title: appLocalizations.override,
widget: const ConfigFragment(),
extendPageWidth: 360,
ListItem.open(
leading: const Icon(Icons.cloud_sync),
title: Text(appLocalizations.backupAndRecovery),
subtitle: Text(appLocalizations.backupAndRecoveryDesc),
delegate: OpenDelegate(
title: appLocalizations.backupAndRecovery,
widget: const BackupAndRecovery(),
),
),
),
ListItem.open(
leading: const Icon(Icons.settings_applications),
title: Text(appLocalizations.application),
subtitle: Text(appLocalizations.applicationDesc),
delegate: OpenDelegate(
title: appLocalizations.application,
widget: const ApplicationSettingFragment(),
),
),
];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final item in items) ...[
item,
if (item != items.last)
const Divider(
height: 0,
if (Platform.isAndroid)
ListItem.open(
leading: const Icon(Icons.view_list),
title: Text(appLocalizations.accessControl),
subtitle: Text(appLocalizations.accessControlDesc),
delegate: OpenDelegate(
title: appLocalizations.appAccessControl,
widget: const AccessFragment(),
),
]
),
ListItem.open(
leading: const Icon(Icons.edit),
title: Text(appLocalizations.override),
subtitle: Text(appLocalizations.overrideDesc),
delegate: OpenDelegate(
title: appLocalizations.override,
widget: const ConfigFragment(),
extendPageWidth: 360,
),
),
ListItem.open(
leading: const Icon(Icons.settings_applications),
title: Text(appLocalizations.application),
subtitle: Text(appLocalizations.applicationDesc),
delegate: OpenDelegate(
title: appLocalizations.application,
widget: const ApplicationSettingFragment(),
),
),
],
);
}
@@ -216,20 +198,16 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
if (state.navigationItems.isEmpty) {
return Container();
}
return Section(
title: appLocalizations.more,
child: _buildNavigationMenu(state.navigationItems),
return Column(
children: [
ListHeader(title: appLocalizations.more),
_buildNavigationMenu(state.navigationItems)
],
);
},
),
Section(
title: appLocalizations.settings,
child: _getSettingList(),
),
Section(
title: appLocalizations.other,
child: _getOtherList(),
),
..._getSettingList(),
..._getOtherList(),
];
return ListView.builder(
itemCount: items.length,

View File

@@ -37,7 +37,7 @@
"overrideDesc": "Override Proxy related config",
"allowLan": "AllowLan",
"allowLanDesc": "Allow access proxy through the LAN",
"tun": "Tun mode",
"tun": "TUN mode",
"tunDesc": "only effective in administrator mode",
"minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Modify the default system exit event",
@@ -191,5 +191,7 @@
"view": "View",
"cut": "Cut",
"copy": "Copy",
"paste": "Paste"
"paste": "Paste",
"testUrl": "Test url",
"sync": "Sync"
}

View File

@@ -37,7 +37,7 @@
"overrideDesc": "覆写代理相关配置",
"allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理",
"tun": "Tun模式",
"tun": "TUN模式",
"tunDesc": "仅在管理员模式生效",
"minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件",
@@ -191,5 +191,7 @@
"view": "查看",
"cut": "剪切",
"copy": "复制",
"paste": "粘贴"
"paste": "粘贴",
"testUrl": "测速链接",
"sync": "同步"
}

View File

@@ -260,6 +260,7 @@ class MessageLookup extends MessageLookupByLibrary {
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
@@ -269,6 +270,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP concurrent"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage(
"Enabling it will allow TCP concurrency"),
"testUrl": MessageLookupByLibrary.simpleMessage("Test url"),
"theme": MessageLookupByLibrary.simpleMessage("Theme"),
"themeColor": MessageLookupByLibrary.simpleMessage("Theme color"),
"themeDesc": MessageLookupByLibrary.simpleMessage(
@@ -277,7 +279,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("Tun mode"),
"tun": MessageLookupByLibrary.simpleMessage("TUN mode"),
"tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"),
"unableToUpdateCurrentProfileDesc":

View File

@@ -209,6 +209,7 @@ class MessageLookup extends MessageLookupByLibrary {
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
@@ -217,6 +218,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
@@ -224,7 +226,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("Tun模式"),
"tun": MessageLookupByLibrary.simpleMessage("TUN模式"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),

View File

@@ -430,10 +430,10 @@ class AppLocalizations {
);
}
/// `Tun mode`
/// `TUN mode`
String get tun {
return Intl.message(
'Tun mode',
'TUN mode',
name: 'tun',
desc: '',
args: [],
@@ -1979,6 +1979,26 @@ class AppLocalizations {
args: [],
);
}
/// `Test url`
String get testUrl {
return Intl.message(
'Test url',
name: 'testUrl',
desc: '',
args: [],
);
}
/// `Sync`
String get sync {
return Intl.message(
'Sync',
name: 'sync',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -6,6 +6,7 @@ import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/tile.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'application.dart';
import 'l10n/l10n.dart';
import 'models/models.dart';
@@ -15,6 +16,7 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await android?.init();
await window?.init();
globalState.packageInfo = await PackageInfo.fromPlatform();
final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final appState = AppState(
@@ -49,11 +51,25 @@ Future<void> vpnService() async {
isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
);
clashMessage.addListener(ClashMessageListenerWithVpn(onTun: (String fd) {
proxyManager.setProtect(
int.parse(fd),
);
}));
clashMessage.addListener(
ClashMessageListenerWithVpn(
onTun: (Fd fd) async {
await proxyManager.setProtect(fd.value);
clashCore.setFdMap(fd.id);
},
onLoaded: (String groupName) {
final currentSelectedMap = config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
if (proxyName == null) return;
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
);
},
),
);
await globalState.init(
appState: appState,
@@ -102,16 +118,24 @@ Future<void> vpnService() async {
}
class ClashMessageListenerWithVpn with ClashMessageListener {
final Function(String fd) _onTun;
final Function(Fd fd) _onTun;
final Function(String) _onLoaded;
ClashMessageListenerWithVpn({
required Function(String fd) onTun,
}) : _onTun = onTun;
required Function(Fd fd) onTun,
required Function(String) onLoaded,
}) : _onTun = onTun,
_onLoaded = onLoaded;
@override
void onTun(String fd) {
void onTun(Fd fd) {
_onTun(fd);
}
@override
void onLoaded(String groupName) {
_onLoaded(groupName);
}
}
class TileListenerWithVpn with TileListener {

View File

@@ -34,6 +34,7 @@ class AppState with ChangeNotifier {
List<Group> _groups;
double _viewWidth;
List<Connection> _requests;
num _checkIpNum;
AppState({
required Mode mode,
@@ -47,6 +48,7 @@ class AppState with ChangeNotifier {
_viewWidth = 0,
_selectedMap = selectedMap,
_sortNum = 0,
_checkIpNum = 0,
_requests = [],
_mode = mode,
_totalTraffic = Traffic(),
@@ -123,9 +125,10 @@ class AppState with ChangeNotifier {
final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return proxyName;
final group = groups[index];
return getRealProxyName(selectedMap.containsKey(proxyName)
? selectedMap[proxyName]
: group.now);
return getRealProxyName((selectedMap.containsKey(proxyName)
? selectedMap[proxyName]
: group.now)) ??
proxyName;
}
String? get showProxyName {
@@ -240,6 +243,15 @@ class AppState with ChangeNotifier {
}
}
num get checkIpNum => _checkIpNum;
set checkIpNum(num value) {
if (_checkIpNum != value) {
_checkIpNum = value;
notifyListeners();
}
}
Mode get mode => _mode;
set mode(Mode value) {

View File

@@ -1,7 +1,9 @@
// ignore_for_file: invalid_annotation_target
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -119,6 +121,7 @@ class ClashConfig extends ChangeNotifier {
Tun _tun;
Dns _dns;
List<String> _rules;
String? _globalRealUa;
ClashConfig()
: _mixedPort = 7890,
@@ -235,7 +238,12 @@ class ClashConfig extends ChangeNotifier {
}
}
Tun get tun => _tun;
Tun get tun {
if (Platform.isAndroid) {
return _tun.copyWith(enable: false);
}
return _tun;
}
set tun(Tun value) {
if (_tun != value) {
@@ -262,6 +270,25 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(name: "global-ua", defaultValue: null)
String get globalUa {
if (_globalRealUa == null) {
return globalState.packageInfo.ua;
} else {
return _globalRealUa!;
}
}
@JsonKey(name: "global-real-ua", defaultValue: null)
String? get globalRealUa => _globalRealUa;
set globalRealUa(String? value) {
if (_globalRealUa != value) {
_globalRealUa = value;
notifyListeners();
}
}
update([ClashConfig? clashConfig]) {
if (clashConfig != null) {
_mixedPort = clashConfig._mixedPort;
@@ -271,6 +298,7 @@ class ClashConfig extends ChangeNotifier {
_tun = clashConfig._tun;
_dns = clashConfig._dns;
_rules = clashConfig._rules;
_globalRealUa = clashConfig.globalRealUa;
}
notifyListeners();
}
@@ -282,9 +310,4 @@ class ClashConfig extends ChangeNotifier {
factory ClashConfig.fromJson(Map<String, dynamic> json) {
return _$ClashConfigFromJson(json);
}
@override
String toString() {
return 'ClashConfig{_mixedPort: $_mixedPort, _allowLan: $_allowLan, _mode: $_mode, _logLevel: $_logLevel, _tun: $_tun, _dns: $_dns, _rules: $_rules}';
}
}

View File

@@ -60,6 +60,7 @@ class Config extends ChangeNotifier {
ProxiesType _proxiesType;
ProxyCardType _proxyCardType;
int _proxiesColumns;
String _testUrl;
Config()
: _profiles = [],
@@ -75,6 +76,7 @@ class Config extends ChangeNotifier {
_isAccessControl = false,
_autoCheckUpdate = true,
_systemProxy = true,
_testUrl = defaultTestUrl,
_accessControl = const AccessControl(),
_isAnimateToPage = true,
_allowBypass = true,
@@ -414,6 +416,17 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(name: "test-url", defaultValue: defaultTestUrl)
String get testUrl => _testUrl;
set testUrl(String value) {
if (_testUrl != value) {
_testUrl = value;
notifyListeners();
}
}
update([
Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -446,6 +459,7 @@ class Config extends ChangeNotifier {
_isAnimateToPage = config._isAnimateToPage;
_autoCheckUpdate = config._autoCheckUpdate;
_dav = config._dav;
_testUrl = config.testUrl;
}
notifyListeners();
}

View File

@@ -3,19 +3,32 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/models/connection.dart';
import 'package:fl_clash/models/models.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/ffi.g.dart';
part 'generated/ffi.freezed.dart';
@freezed
class ConfigExtendedParams with _$ConfigExtendedParams {
const factory ConfigExtendedParams({
@JsonKey(name: "is-patch") required bool isPatch,
@JsonKey(name: "is-compatible") required bool isCompatible,
@JsonKey(name: "selected-map") required SelectedMap selectedMap,
@JsonKey(name: "test-url") required String testUrl,
}) = _ConfigExtendedParams;
factory ConfigExtendedParams.fromJson(Map<String, Object?> json) =>
_$ConfigExtendedParamsFromJson(json);
}
@freezed
class UpdateConfigParams with _$UpdateConfigParams {
const factory UpdateConfigParams({
@JsonKey(name: "profile-path") String? profilePath,
required ClashConfig config,
@JsonKey(name: "is-patch") required bool isPatch,
@JsonKey(name: "is-compatible") required bool isCompatible,
required ConfigExtendedParams params,
}) = _UpdateConfigParams;
factory UpdateConfigParams.fromJson(Map<String, Object?> json) =>
@@ -75,6 +88,16 @@ class Process with _$Process {
_$ProcessFromJson(json);
}
@freezed
class Fd with _$Fd {
const factory Fd({
required int id,
required int value,
}) = _Fd;
factory Fd.fromJson(Map<String, Object?> json) => _$FdFromJson(json);
}
@freezed
class ProcessMapItem with _$ProcessMapItem {
const factory ProcessMapItem({

View File

@@ -51,7 +51,8 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
..tcpConcurrent = json['tcp-concurrent'] as bool? ?? false
..tun = Tun.fromJson(json['tun'] as Map<String, dynamic>)
..dns = Dns.fromJson(json['dns'] as Map<String, dynamic>)
..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList();
..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList()
..globalRealUa = json['global-real-ua'] as String?;
Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
<String, dynamic>{
@@ -68,6 +69,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'tun': instance.tun,
'dns': instance.dns,
'rules': instance.rules,
'global-real-ua': instance.globalRealUa,
};
const _$ModeEnumMap = {

View File

@@ -41,7 +41,9 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2;
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2
..testUrl =
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204';
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -66,6 +68,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!,
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
'proxiesColumns': instance.proxiesColumns,
'test-url': instance.testUrl,
};
const _$ThemeModeEnumMap = {

View File

@@ -14,6 +14,234 @@ T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
ConfigExtendedParams _$ConfigExtendedParamsFromJson(Map<String, dynamic> json) {
return _ConfigExtendedParams.fromJson(json);
}
/// @nodoc
mixin _$ConfigExtendedParams {
@JsonKey(name: "is-patch")
bool get isPatch => throw _privateConstructorUsedError;
@JsonKey(name: "is-compatible")
bool get isCompatible => throw _privateConstructorUsedError;
@JsonKey(name: "selected-map")
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
@JsonKey(name: "test-url")
String get testUrl => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ConfigExtendedParamsCopyWith<ConfigExtendedParams> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ConfigExtendedParamsCopyWith<$Res> {
factory $ConfigExtendedParamsCopyWith(ConfigExtendedParams value,
$Res Function(ConfigExtendedParams) then) =
_$ConfigExtendedParamsCopyWithImpl<$Res, ConfigExtendedParams>;
@useResult
$Res call(
{@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible,
@JsonKey(name: "selected-map") Map<String, String> selectedMap,
@JsonKey(name: "test-url") String testUrl});
}
/// @nodoc
class _$ConfigExtendedParamsCopyWithImpl<$Res,
$Val extends ConfigExtendedParams>
implements $ConfigExtendedParamsCopyWith<$Res> {
_$ConfigExtendedParamsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isPatch = null,
Object? isCompatible = null,
Object? selectedMap = null,
Object? testUrl = null,
}) {
return _then(_value.copyWith(
isPatch: null == isPatch
? _value.isPatch
: isPatch // ignore: cast_nullable_to_non_nullable
as bool,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
testUrl: null == testUrl
? _value.testUrl
: testUrl // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ConfigExtendedParamsImplCopyWith<$Res>
implements $ConfigExtendedParamsCopyWith<$Res> {
factory _$$ConfigExtendedParamsImplCopyWith(_$ConfigExtendedParamsImpl value,
$Res Function(_$ConfigExtendedParamsImpl) then) =
__$$ConfigExtendedParamsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible,
@JsonKey(name: "selected-map") Map<String, String> selectedMap,
@JsonKey(name: "test-url") String testUrl});
}
/// @nodoc
class __$$ConfigExtendedParamsImplCopyWithImpl<$Res>
extends _$ConfigExtendedParamsCopyWithImpl<$Res, _$ConfigExtendedParamsImpl>
implements _$$ConfigExtendedParamsImplCopyWith<$Res> {
__$$ConfigExtendedParamsImplCopyWithImpl(_$ConfigExtendedParamsImpl _value,
$Res Function(_$ConfigExtendedParamsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isPatch = null,
Object? isCompatible = null,
Object? selectedMap = null,
Object? testUrl = null,
}) {
return _then(_$ConfigExtendedParamsImpl(
isPatch: null == isPatch
? _value.isPatch
: isPatch // ignore: cast_nullable_to_non_nullable
as bool,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
testUrl: null == testUrl
? _value.testUrl
: testUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams {
const _$ConfigExtendedParamsImpl(
{@JsonKey(name: "is-patch") required this.isPatch,
@JsonKey(name: "is-compatible") required this.isCompatible,
@JsonKey(name: "selected-map")
required final Map<String, String> selectedMap,
@JsonKey(name: "test-url") required this.testUrl})
: _selectedMap = selectedMap;
factory _$ConfigExtendedParamsImpl.fromJson(Map<String, dynamic> json) =>
_$$ConfigExtendedParamsImplFromJson(json);
@override
@JsonKey(name: "is-patch")
final bool isPatch;
@override
@JsonKey(name: "is-compatible")
final bool isCompatible;
final Map<String, String> _selectedMap;
@override
@JsonKey(name: "selected-map")
Map<String, String> get selectedMap {
if (_selectedMap is EqualUnmodifiableMapView) return _selectedMap;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_selectedMap);
}
@override
@JsonKey(name: "test-url")
final String testUrl;
@override
String toString() {
return 'ConfigExtendedParams(isPatch: $isPatch, isCompatible: $isCompatible, selectedMap: $selectedMap, testUrl: $testUrl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ConfigExtendedParamsImpl &&
(identical(other.isPatch, isPatch) || other.isPatch == isPatch) &&
(identical(other.isCompatible, isCompatible) ||
other.isCompatible == isCompatible) &&
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap) &&
(identical(other.testUrl, testUrl) || other.testUrl == testUrl));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, isPatch, isCompatible,
const DeepCollectionEquality().hash(_selectedMap), testUrl);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ConfigExtendedParamsImplCopyWith<_$ConfigExtendedParamsImpl>
get copyWith =>
__$$ConfigExtendedParamsImplCopyWithImpl<_$ConfigExtendedParamsImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ConfigExtendedParamsImplToJson(
this,
);
}
}
abstract class _ConfigExtendedParams implements ConfigExtendedParams {
const factory _ConfigExtendedParams(
{@JsonKey(name: "is-patch") required final bool isPatch,
@JsonKey(name: "is-compatible") required final bool isCompatible,
@JsonKey(name: "selected-map")
required final Map<String, String> selectedMap,
@JsonKey(name: "test-url") required final String testUrl}) =
_$ConfigExtendedParamsImpl;
factory _ConfigExtendedParams.fromJson(Map<String, dynamic> json) =
_$ConfigExtendedParamsImpl.fromJson;
@override
@JsonKey(name: "is-patch")
bool get isPatch;
@override
@JsonKey(name: "is-compatible")
bool get isCompatible;
@override
@JsonKey(name: "selected-map")
Map<String, String> get selectedMap;
@override
@JsonKey(name: "test-url")
String get testUrl;
@override
@JsonKey(ignore: true)
_$$ConfigExtendedParamsImplCopyWith<_$ConfigExtendedParamsImpl>
get copyWith => throw _privateConstructorUsedError;
}
UpdateConfigParams _$UpdateConfigParamsFromJson(Map<String, dynamic> json) {
return _UpdateConfigParams.fromJson(json);
}
@@ -23,10 +251,7 @@ mixin _$UpdateConfigParams {
@JsonKey(name: "profile-path")
String? get profilePath => throw _privateConstructorUsedError;
ClashConfig get config => throw _privateConstructorUsedError;
@JsonKey(name: "is-patch")
bool get isPatch => throw _privateConstructorUsedError;
@JsonKey(name: "is-compatible")
bool get isCompatible => throw _privateConstructorUsedError;
ConfigExtendedParams get params => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -43,8 +268,9 @@ abstract class $UpdateConfigParamsCopyWith<$Res> {
$Res call(
{@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config,
@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible});
ConfigExtendedParams params});
$ConfigExtendedParamsCopyWith<$Res> get params;
}
/// @nodoc
@@ -62,8 +288,7 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
$Res call({
Object? profilePath = freezed,
Object? config = null,
Object? isPatch = null,
Object? isCompatible = null,
Object? params = null,
}) {
return _then(_value.copyWith(
profilePath: freezed == profilePath
@@ -74,16 +299,20 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
? _value.config
: config // ignore: cast_nullable_to_non_nullable
as ClashConfig,
isPatch: null == isPatch
? _value.isPatch
: isPatch // ignore: cast_nullable_to_non_nullable
as bool,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
params: null == params
? _value.params
: params // ignore: cast_nullable_to_non_nullable
as ConfigExtendedParams,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$ConfigExtendedParamsCopyWith<$Res> get params {
return $ConfigExtendedParamsCopyWith<$Res>(_value.params, (value) {
return _then(_value.copyWith(params: value) as $Val);
});
}
}
/// @nodoc
@@ -97,8 +326,10 @@ abstract class _$$UpdateConfigParamsImplCopyWith<$Res>
$Res call(
{@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config,
@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible});
ConfigExtendedParams params});
@override
$ConfigExtendedParamsCopyWith<$Res> get params;
}
/// @nodoc
@@ -114,8 +345,7 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
$Res call({
Object? profilePath = freezed,
Object? config = null,
Object? isPatch = null,
Object? isCompatible = null,
Object? params = null,
}) {
return _then(_$UpdateConfigParamsImpl(
profilePath: freezed == profilePath
@@ -126,14 +356,10 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
? _value.config
: config // ignore: cast_nullable_to_non_nullable
as ClashConfig,
isPatch: null == isPatch
? _value.isPatch
: isPatch // ignore: cast_nullable_to_non_nullable
as bool,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
params: null == params
? _value.params
: params // ignore: cast_nullable_to_non_nullable
as ConfigExtendedParams,
));
}
}
@@ -144,8 +370,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
const _$UpdateConfigParamsImpl(
{@JsonKey(name: "profile-path") this.profilePath,
required this.config,
@JsonKey(name: "is-patch") required this.isPatch,
@JsonKey(name: "is-compatible") required this.isCompatible});
required this.params});
factory _$UpdateConfigParamsImpl.fromJson(Map<String, dynamic> json) =>
_$$UpdateConfigParamsImplFromJson(json);
@@ -156,15 +381,11 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
@override
final ClashConfig config;
@override
@JsonKey(name: "is-patch")
final bool isPatch;
@override
@JsonKey(name: "is-compatible")
final bool isCompatible;
final ConfigExtendedParams params;
@override
String toString() {
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, isPatch: $isPatch, isCompatible: $isCompatible)';
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, params: $params)';
}
@override
@@ -175,15 +396,12 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
(identical(other.profilePath, profilePath) ||
other.profilePath == profilePath) &&
(identical(other.config, config) || other.config == config) &&
(identical(other.isPatch, isPatch) || other.isPatch == isPatch) &&
(identical(other.isCompatible, isCompatible) ||
other.isCompatible == isCompatible));
(identical(other.params, params) || other.params == params));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, profilePath, config, isPatch, isCompatible);
int get hashCode => Object.hash(runtimeType, profilePath, config, params);
@JsonKey(ignore: true)
@override
@@ -202,11 +420,9 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
abstract class _UpdateConfigParams implements UpdateConfigParams {
const factory _UpdateConfigParams(
{@JsonKey(name: "profile-path") final String? profilePath,
required final ClashConfig config,
@JsonKey(name: "is-patch") required final bool isPatch,
@JsonKey(name: "is-compatible") required final bool isCompatible}) =
_$UpdateConfigParamsImpl;
{@JsonKey(name: "profile-path") final String? profilePath,
required final ClashConfig config,
required final ConfigExtendedParams params}) = _$UpdateConfigParamsImpl;
factory _UpdateConfigParams.fromJson(Map<String, dynamic> json) =
_$UpdateConfigParamsImpl.fromJson;
@@ -217,11 +433,7 @@ abstract class _UpdateConfigParams implements UpdateConfigParams {
@override
ClashConfig get config;
@override
@JsonKey(name: "is-patch")
bool get isPatch;
@override
@JsonKey(name: "is-compatible")
bool get isCompatible;
ConfigExtendedParams get params;
@override
@JsonKey(ignore: true)
_$$UpdateConfigParamsImplCopyWith<_$UpdateConfigParamsImpl> get copyWith =>
@@ -1006,6 +1218,151 @@ abstract class _Process implements Process {
throw _privateConstructorUsedError;
}
Fd _$FdFromJson(Map<String, dynamic> json) {
return _Fd.fromJson(json);
}
/// @nodoc
mixin _$Fd {
int get id => throw _privateConstructorUsedError;
int get value => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$FdCopyWith<Fd> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $FdCopyWith<$Res> {
factory $FdCopyWith(Fd value, $Res Function(Fd) then) =
_$FdCopyWithImpl<$Res, Fd>;
@useResult
$Res call({int id, int value});
}
/// @nodoc
class _$FdCopyWithImpl<$Res, $Val extends Fd> implements $FdCopyWith<$Res> {
_$FdCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? value = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$FdImplCopyWith<$Res> implements $FdCopyWith<$Res> {
factory _$$FdImplCopyWith(_$FdImpl value, $Res Function(_$FdImpl) then) =
__$$FdImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int id, int value});
}
/// @nodoc
class __$$FdImplCopyWithImpl<$Res> extends _$FdCopyWithImpl<$Res, _$FdImpl>
implements _$$FdImplCopyWith<$Res> {
__$$FdImplCopyWithImpl(_$FdImpl _value, $Res Function(_$FdImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? value = null,
}) {
return _then(_$FdImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$FdImpl implements _Fd {
const _$FdImpl({required this.id, required this.value});
factory _$FdImpl.fromJson(Map<String, dynamic> json) =>
_$$FdImplFromJson(json);
@override
final int id;
@override
final int value;
@override
String toString() {
return 'Fd(id: $id, value: $value)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FdImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.value, value) || other.value == value));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, id, value);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$FdImplCopyWith<_$FdImpl> get copyWith =>
__$$FdImplCopyWithImpl<_$FdImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$FdImplToJson(
this,
);
}
}
abstract class _Fd implements Fd {
const factory _Fd({required final int id, required final int value}) =
_$FdImpl;
factory _Fd.fromJson(Map<String, dynamic> json) = _$FdImpl.fromJson;
@override
int get id;
@override
int get value;
@override
@JsonKey(ignore: true)
_$$FdImplCopyWith<_$FdImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ProcessMapItem _$ProcessMapItemFromJson(Map<String, dynamic> json) {
return _ProcessMapItem.fromJson(json);
}

View File

@@ -6,13 +6,31 @@ part of '../ffi.dart';
// JsonSerializableGenerator
// **************************************************************************
_$ConfigExtendedParamsImpl _$$ConfigExtendedParamsImplFromJson(
Map<String, dynamic> json) =>
_$ConfigExtendedParamsImpl(
isPatch: json['is-patch'] as bool,
isCompatible: json['is-compatible'] as bool,
selectedMap: Map<String, String>.from(json['selected-map'] as Map),
testUrl: json['test-url'] as String,
);
Map<String, dynamic> _$$ConfigExtendedParamsImplToJson(
_$ConfigExtendedParamsImpl instance) =>
<String, dynamic>{
'is-patch': instance.isPatch,
'is-compatible': instance.isCompatible,
'selected-map': instance.selectedMap,
'test-url': instance.testUrl,
};
_$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson(
Map<String, dynamic> json) =>
_$UpdateConfigParamsImpl(
profilePath: json['profile-path'] as String?,
config: ClashConfig.fromJson(json['config'] as Map<String, dynamic>),
isPatch: json['is-patch'] as bool,
isCompatible: json['is-compatible'] as bool,
params:
ConfigExtendedParams.fromJson(json['params'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
@@ -20,8 +38,7 @@ Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
<String, dynamic>{
'profile-path': instance.profilePath,
'config': instance.config,
'is-patch': instance.isPatch,
'is-compatible': instance.isCompatible,
'params': instance.params,
};
_$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson(
@@ -58,6 +75,7 @@ const _$MessageTypeEnumMap = {
MessageType.now: 'now',
MessageType.request: 'request',
MessageType.run: 'run',
MessageType.loaded: 'loaded',
};
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(
@@ -93,6 +111,16 @@ Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
'metadata': instance.metadata,
};
_$FdImpl _$$FdImplFromJson(Map<String, dynamic> json) => _$FdImpl(
id: (json['id'] as num).toInt(),
value: (json['value'] as num).toInt(),
);
Map<String, dynamic> _$$FdImplToJson(_$FdImpl instance) => <String, dynamic>{
'id': instance.id,
'value': instance.value,
};
_$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) =>
_$ProcessMapItemImpl(
id: (json['id'] as num).toInt(),

View File

@@ -161,6 +161,7 @@ mixin _$CheckIpSelectorState {
bool get isInit => throw _privateConstructorUsedError;
bool get isStart => throw _privateConstructorUsedError;
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
num get checkIpNum => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$CheckIpSelectorStateCopyWith<CheckIpSelectorState> get copyWith =>
@@ -173,7 +174,11 @@ abstract class $CheckIpSelectorStateCopyWith<$Res> {
$Res Function(CheckIpSelectorState) then) =
_$CheckIpSelectorStateCopyWithImpl<$Res, CheckIpSelectorState>;
@useResult
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap});
$Res call(
{bool isInit,
bool isStart,
Map<String, String> selectedMap,
num checkIpNum});
}
/// @nodoc
@@ -193,6 +198,7 @@ class _$CheckIpSelectorStateCopyWithImpl<$Res,
Object? isInit = null,
Object? isStart = null,
Object? selectedMap = null,
Object? checkIpNum = null,
}) {
return _then(_value.copyWith(
isInit: null == isInit
@@ -207,6 +213,10 @@ class _$CheckIpSelectorStateCopyWithImpl<$Res,
? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
checkIpNum: null == checkIpNum
? _value.checkIpNum
: checkIpNum // ignore: cast_nullable_to_non_nullable
as num,
) as $Val);
}
}
@@ -219,7 +229,11 @@ abstract class _$$CheckIpSelectorStateImplCopyWith<$Res>
__$$CheckIpSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap});
$Res call(
{bool isInit,
bool isStart,
Map<String, String> selectedMap,
num checkIpNum});
}
/// @nodoc
@@ -236,6 +250,7 @@ class __$$CheckIpSelectorStateImplCopyWithImpl<$Res>
Object? isInit = null,
Object? isStart = null,
Object? selectedMap = null,
Object? checkIpNum = null,
}) {
return _then(_$CheckIpSelectorStateImpl(
isInit: null == isInit
@@ -250,6 +265,10 @@ class __$$CheckIpSelectorStateImplCopyWithImpl<$Res>
? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
checkIpNum: null == checkIpNum
? _value.checkIpNum
: checkIpNum // ignore: cast_nullable_to_non_nullable
as num,
));
}
}
@@ -260,7 +279,8 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
const _$CheckIpSelectorStateImpl(
{required this.isInit,
required this.isStart,
required final Map<String, String> selectedMap})
required final Map<String, String> selectedMap,
required this.checkIpNum})
: _selectedMap = selectedMap;
@override
@@ -275,9 +295,12 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
return EqualUnmodifiableMapView(_selectedMap);
}
@override
final num checkIpNum;
@override
String toString() {
return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap)';
return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap, checkIpNum: $checkIpNum)';
}
@override
@@ -288,12 +311,14 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
(identical(other.isInit, isInit) || other.isInit == isInit) &&
(identical(other.isStart, isStart) || other.isStart == isStart) &&
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap));
.equals(other._selectedMap, _selectedMap) &&
(identical(other.checkIpNum, checkIpNum) ||
other.checkIpNum == checkIpNum));
}
@override
int get hashCode => Object.hash(runtimeType, isInit, isStart,
const DeepCollectionEquality().hash(_selectedMap));
const DeepCollectionEquality().hash(_selectedMap), checkIpNum);
@JsonKey(ignore: true)
@override
@@ -306,10 +331,10 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
abstract class _CheckIpSelectorState implements CheckIpSelectorState {
const factory _CheckIpSelectorState(
{required final bool isInit,
required final bool isStart,
required final Map<String, String> selectedMap}) =
_$CheckIpSelectorStateImpl;
{required final bool isInit,
required final bool isStart,
required final Map<String, String> selectedMap,
required final num checkIpNum}) = _$CheckIpSelectorStateImpl;
@override
bool get isInit;
@@ -318,6 +343,8 @@ abstract class _CheckIpSelectorState implements CheckIpSelectorState {
@override
Map<String, String> get selectedMap;
@override
num get checkIpNum;
@override
@JsonKey(ignore: true)
_$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;

View File

@@ -19,6 +19,7 @@ class CheckIpSelectorState with _$CheckIpSelectorState {
required bool isInit,
required bool isStart,
required SelectedMap selectedMap,
required num checkIpNum
}) = _CheckIpSelectorState;
}

View File

@@ -1,24 +1,30 @@
import 'package:fl_clash/common/constant.dart';
import 'package:flutter/material.dart';
@immutable
class SystemColorSchemes {
SystemColorSchemes({
ColorScheme? lightColorScheme,
ColorScheme? darkColorScheme,
}) : lightColorScheme = lightColorScheme ??
ColorScheme.fromSeed(seedColor: defaultPrimaryColor),
darkColorScheme = darkColorScheme ??
ColorScheme.fromSeed(
seedColor: defaultPrimaryColor,
brightness: Brightness.dark,
);
ColorScheme lightColorScheme;
ColorScheme darkColorScheme;
final ColorScheme? lightColorScheme;
final ColorScheme? darkColorScheme;
const SystemColorSchemes({
this.lightColorScheme,
this.darkColorScheme,
});
getSystemColorSchemeForBrightness(Brightness? brightness) {
if (brightness != null && brightness == Brightness.dark) {
return darkColorScheme;
return darkColorScheme != null
? ColorScheme.fromSeed(
seedColor: darkColorScheme!.primary,
brightness: brightness,
)
: ColorScheme.fromSeed(
seedColor: defaultPrimaryColor,
brightness: brightness,
);
}
return lightColorScheme;
return lightColorScheme != null
? ColorScheme.fromSeed(seedColor: darkColorScheme!.primary)
: ColorScheme.fromSeed(seedColor: defaultPrimaryColor);
}
}

View File

@@ -7,6 +7,7 @@ import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'controller.dart';
import 'models/models.dart';
@@ -15,6 +16,7 @@ import 'common/common.dart';
class GlobalState {
Timer? timer;
Timer? groupsUpdateTimer;
late PackageInfo packageInfo;
Function? updateCurrentDelayDebounce;
PageController? pageController;
final navigatorKey = GlobalKey<NavigatorState>();
@@ -43,13 +45,18 @@ class GlobalState {
}) async {
final profilePath = await appPath.getProfilePath(config.currentProfileId);
await config.currentProfile?.checkAndUpdate();
debugPrint("update config");
final res = await clashCore.updateConfig(UpdateConfigParams(
profilePath: profilePath,
config: clashConfig,
isPatch: isPatch,
isCompatible: config.isCompatible,
));
final res = await clashCore.updateConfig(
UpdateConfigParams(
profilePath: profilePath,
config: clashConfig,
params: ConfigExtendedParams(
isPatch: isPatch,
isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
testUrl: config.testUrl,
),
),
);
if (res.isNotEmpty) throw res;
}
@@ -79,7 +86,9 @@ class GlobalState {
appState: appState,
config: config,
clashConfig: clashConfig,
);
).then((_){
appController.addCheckIpNumDebounce();
});
}
Future<void> stopSystemProxy() async {
@@ -87,7 +96,7 @@ class GlobalState {
stopListenUpdate();
}
Future<void> applyProfile({
Future applyProfile({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
@@ -98,11 +107,6 @@ class GlobalState {
isPatch: false,
);
await updateGroups(appState);
changeProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
init({
@@ -120,25 +124,6 @@ class GlobalState {
updateCoreVersionInfo(appState);
}
changeProxy({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) {
if (config.profiles.isEmpty) {
stopSystemProxy();
return;
}
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: key,
proxyName: value,
),
);
});
}
Future<void> updateGroups(AppState appState) async {
appState.groups = await clashCore.getProxiesGroups();
}
@@ -263,7 +248,7 @@ class GlobalState {
}
}
int getColumns(ViewMode viewMode,int currentColumns){
int getColumns(ViewMode viewMode, int currentColumns) {
final targetColumnsArray = switch (viewMode) {
ViewMode.mobile => [2, 1],
ViewMode.laptop => [3, 2],

View File

@@ -20,7 +20,7 @@ class CommonChip extends StatelessWidget {
if (type == ChipType.delete) {
return Chip(
avatar: avatar,
padding: const EdgeInsets.symmetric(
labelPadding:const EdgeInsets.symmetric(
vertical: 0,
horizontal: 4,
),
@@ -35,7 +35,7 @@ class CommonChip extends StatelessWidget {
return ActionChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
avatar: avatar,
padding: const EdgeInsets.symmetric(
labelPadding:const EdgeInsets.symmetric(
vertical: 0,
horizontal: 4,
),

View File

@@ -1,7 +1,6 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/plugins/app.dart';
@@ -51,8 +50,9 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
}
@override
void onTun(String fd) {
proxyManager.setProtect(int.parse(fd));
Future<void> onTun(Fd fd) async {
await proxyManager.setProtect(fd.value);
clashCore.setFdMap(fd.id);
super.onTun(fd);
}
@@ -75,8 +75,18 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
}
@override
void onRun(String runTime) async {
// proxy?.updateStartTime();
super.onRun(runTime);
void onLoaded(String groupName) {
final appController = globalState.appController;
final currentSelectedMap = appController.config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
if (proxyName == null) return;
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
);
appController.addCheckIpNumDebounce();
super.onLoaded(proxyName);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/open_container.dart';
@@ -214,7 +215,8 @@ class ListItem<T> extends StatelessWidget {
return OpenContainer(
closedBuilder: (_, action) {
openAction() {
final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile;
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
if (!isMobile) {
showExtendPage(
context,
@@ -243,7 +245,8 @@ class ListItem<T> extends StatelessWidget {
final nextDelegate = delegate as NextDelegate;
return _buildListTile(
onTab: () {
final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile;
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
if (!isMobile) {
showExtendPage(
context,
@@ -319,3 +322,77 @@ class ListItem<T> extends StatelessWidget {
);
}
}
class ListHeader extends StatelessWidget {
final String title;
final List<Widget> actions;
const ListHeader({
super.key,
required this.title,
List<Widget>? actions,
}) : actions = actions ?? const [];
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
Expanded(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
...actions,
],
),
),
],
),
);
}
}
List<Widget> generateSection({
required String title,
required Iterable<Widget> items,
List<Widget>? actions,
bool separated = true,
}) {
final genItems = separated
? items.separated(
const Divider(
height: 0,
),
)
: items;
return [
if (items.isNotEmpty)
ListHeader(
title: title,
actions: actions,
),
...genItems,
];
}
Widget generateListView(List<Widget> items) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) => items[index],
);
}

View File

@@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
class Section extends StatelessWidget {
final String title;
final Widget child;
const Section({
super.key,
required this.title,
required this.child,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
Expanded(
flex: 0,
child: child,
)
],
);
}
}

View File

@@ -22,5 +22,4 @@ export 'tile_container.dart';
export 'chip.dart';
export 'fade_box.dart';
export 'app_state_container.dart';
export 'text.dart';
export 'section.dart';
export 'text.dart';

View File

@@ -229,10 +229,18 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5"
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
url: "https://pub.dev"
source: hosted
version: "5.4.3+1"
version: "5.5.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
dynamic_color:
dependency: "direct main"
description:
@@ -277,10 +285,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "2ca051989f69d1b2ca012b2cf3ccf78c70d40144f0861ff2c063493f7c8c3d45"
sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258"
url: "https://pub.dev"
source: hosted
version: "8.0.5"
version: "8.0.6"
file_selector_linux:
dependency: transitive
description:
@@ -369,10 +377,10 @@ packages:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2"
frontend_server_client:
dependency: transitive
description:
@@ -630,7 +638,7 @@ packages:
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
dependency: "direct main"
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
@@ -1162,7 +1170,7 @@ packages:
source: hosted
version: "1.2.2"
win32:
dependency: transitive
dependency: "direct main"
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4

View File

@@ -1,7 +1,7 @@
name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none'
version: 0.8.31
version: 0.8.35+202407071
environment:
sdk: '>=3.1.0 <4.0.0'
@@ -16,7 +16,6 @@ dependencies:
shared_preferences: ^2.2.0
provider: ^6.0.5
window_manager: ^0.3.8
ffi: ^2.1.0
dynamic_color: ^1.7.0
proxy:
path: plugins/proxy
@@ -41,6 +40,9 @@ dependencies:
country_flags: ^2.2.0
re_editor: ^0.3.0
re_highlight: ^0.0.3
win32: ^5.5.1
ffi: ^2.1.2
material_color_utilities: ^0.8.0
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -1,67 +1,10 @@
// ignore_for_file: avoid_print
import 'package:fl_clash/common/common.dart';
void main() async {
String input = """
<details markdown=1><summary>All changes from v0.8.5 to the latest commit:</summary>
(unreleased)
------------
- Fix submit error. [chen08209]
- Add WebDAV. [chen08209]
add Auto check updates
Optimize more details
- Optimize delayTest. [chen08209]
- Upgrade flutter version. [chen08209]
- Update kernel Add import profile via QR code image. [chen08209]
- Add compatibility mode and adapt clash scheme. [chen08209]
- Update Version. [chen08209]
- Reconstruction application proxy logic. [chen08209]
- Fix Tab destroy error. [chen08209]
- Optimize repeat healthcheck. [chen08209]
- Optimize Direct mode ui. [chen08209]
- Optimize Healthcheck. [chen08209]
- Remove proxies position animation, improve performance Add Telegram
Link. [chen08209]
- Update healthcheck policy. [chen08209]
- New Check URLTest. [chen08209]
- Fix the problem of invalid auto-selection. [chen08209]
- New Async UpdateConfig. [chen08209]
- Add changeProfileDebounce. [chen08209]
- Update Workflow. [chen08209]
- Fix ChangeProfile block. [chen08209]
- Fix Release Message Error. [chen08209]
- Update Selector 2. [chen08209]
- Update Version. [chen08209]
- Fix Proxies Select Error. [chen08209]
- Fix the problem that the proxy group is empty in global mode.
[chen08209]
- Fix the problem that the proxy group is empty in global mode.
[chen08209]
- Add ProxyProvider2. [chen08209]
- Add ProxyProvider. [chen08209]
- Update Version. [chen08209]
- Update ProxyGroup Sort. [chen08209]
- Fix Android quickStart VpnService some problems. [chen08209]
- Update version. [chen08209]
- Set Android notification low importance. [chen08209]
- Fix the issue that VpnService can't be closed correctly in special
cases. [chen08209]
- Fix the problem that TileService is not destroyed correctly in some
cases. [chen08209]
Adjust tab animation defaults
- Add Telegram in README_zh_CN.md. [chen08209]
- Add Telegram. [chen08209]
""";
const pattern = r'- (.+?)\. \[.+?\]';
final regex = RegExp(pattern);
for (final match in regex.allMatches(input)) {
final change = match.group(1);
print(change);
}
print("https://pqjc.site:10000/test.ymal".isUrl);
print("abcd".isUrl);
print("http://10.31.1.221:8848/cfa.yaml".isUrl);
}

View File

@@ -17,6 +17,11 @@ add_executable(${BINARY_NAME} WIN32
"Runner.rc"
"runner.exe.manifest"
)
# add_executable(service
# "service.cpp"
# )
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})

152
windows/runner/service.cpp Normal file
View File

@@ -0,0 +1,152 @@
#include <windows.h>
#include <tchar.h>
#include <string>
#define SERVICE_NAME _T("MyService")
SERVICE_STATUS g_ServiceStatus = {0};
SERVICE_STATUS_HANDLE g_StatusHandle = NULL;
HANDLE g_ServiceStopEvent = INVALID_HANDLE_VALUE;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv);
VOID WINAPI ServiceCtrlHandler(DWORD);
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam);
int _tmain(int argc, TCHAR *argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[] =
{
{SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION) ServiceMain},
{NULL, NULL}
};
if (StartServiceCtrlDispatcher(ServiceTable) == FALSE)
{
return GetLastError();
}
return 0;
}
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv)
{
DWORD Status = E_FAIL;
g_StatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME, ServiceCtrlHandler);
if (g_StatusHandle == NULL)
{
goto EXIT;
}
ZeroMemory(&g_ServiceStatus, sizeof(g_ServiceStatus));
g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
g_ServiceStatus.dwWin32ExitCode = 0;
g_ServiceStatus.dwServiceSpecificExitCode = 0;
g_ServiceStatus.dwCheckPoint = 0;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
g_ServiceStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (g_ServiceStopEvent == NULL)
{
g_ServiceStatus.dwControlsAccepted = 0;
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
g_ServiceStatus.dwWin32ExitCode = GetLastError();
g_ServiceStatus.dwCheckPoint = 1;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
goto EXIT;
}
g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
g_ServiceStatus.dwCheckPoint = 0;
g_ServiceStatus.dwWaitHint = 0;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
HANDLE hThread = CreateThread(NULL, 0, ServiceWorkerThread, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(g_ServiceStopEvent);
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
g_ServiceStatus.dwCheckPoint = 3;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
EXIT:
return;
}
VOID WINAPI ServiceCtrlHandler(DWORD CtrlCode)
{
switch(CtrlCode)
{
case SERVICE_CONTROL_STOP:
if (g_ServiceStatus.dwCurrentState != SERVICE_RUNNING)
break;
g_ServiceStatus.dwControlsAccepted = 0;
g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
g_ServiceStatus.dwWin32ExitCode = 0;
g_ServiceStatus.dwCheckPoint = 4;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceCtrlHandler: SetServiceStatus returned error"));
}
SetEvent(g_ServiceStopEvent);
break;
default:
break;
}
}
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
{
while(WaitForSingleObject(g_ServiceStopEvent, 0) != WAIT_OBJECT_0)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// 启动 "C:\path\to\your\executable.exe"
if(!CreateProcess(NULL, _T("C:\\path\\to\\your\\executable.exe"), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
OutputDebugString(_T("CreateProcess failed"));
}
// 等待进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
// 关闭进程和线程句柄
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
// 每隔一段时间检查一次这里设置为60秒
Sleep(60000);
}
return ERROR_SUCCESS;
}