Compare commits

..

1 Commits

Author SHA1 Message Date
chen08209
48391ecbbf Fix scroll physics error 2025-02-09 16:51:30 +08:00
213 changed files with 11284 additions and 24295 deletions

View File

@@ -4,8 +4,6 @@ on:
push:
tags:
- 'v*'
env:
IS_STABLE: ${{ !contains(github.ref, '-') }}
jobs:
build:
@@ -46,7 +44,7 @@ jobs:
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r28
ndk-version: r26b
add-to-path: true
link-to-sdk: true
@@ -69,6 +67,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: 3.24.5
channel: stable
cache: true
@@ -76,7 +75,7 @@ jobs:
run: flutter pub get
- name: Setup
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }}
- name: Upload
uses: actions/upload-artifact@v4
@@ -90,13 +89,14 @@ jobs:
needs: [ build ]
steps:
- name: Checkout
if: ${{ !contains(github.ref, '+') }}
uses: actions/checkout@v4
if: ${{ env.IS_STABLE == 'true' }}
with:
fetch-depth: 0
ref: refs/heads/main
- name: Generate
if: ${{ env.IS_STABLE == 'true' }}
if: ${{ !contains(github.ref, '+') }}
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
@@ -128,7 +128,7 @@ jobs:
cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit
if: ${{ env.IS_STABLE == 'true' }}
if: ${{ !contains(github.ref, '+') }}
run: |
git add CHANGELOG.md
if ! git diff --cached --quiet; then
@@ -207,7 +207,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install requests
python release_telegram.py
python release.py
- name: Patch release.md
run: |
@@ -215,21 +215,21 @@ jobs:
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
- name: Release
if: ${{ env.IS_STABLE == 'true' }}
if: ${{ !contains(github.ref, '+') }}
uses: softprops/action-gh-release@v2
with:
files: ./dist/*
body_path: './release.md'
- name: Create Fdroid Source Dir
if: ${{ env.IS_STABLE == 'true' }}
if: ${{ !contains(github.ref, '+') }}
run: |
mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully"
- name: Push to fdroid repo
if: ${{ env.IS_STABLE == 'true' }}
if: ${{ !contains(github.ref, '+') }}
uses: cpina/github-action-push-to-another-repository@v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
@@ -239,7 +239,7 @@ jobs:
destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: main
target-branch: action-pr
commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/

4
.gitmodules vendored
View File

@@ -1,10 +1,8 @@
[submodule "core/Clash.Meta"]
path = core/Clash.Meta
url = git@github.com:chen08209/Clash.Meta.git
branch = FlClash
branch = FlClash-Alpha
[submodule "plugins/flutter_distributor"]
path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git
branch = FlClash

View File

@@ -1,77 +1,3 @@
## v0.8.81
- Add rule override
- Update core
- Optimize more details
- Update changelog
## v0.8.80
- Optimize dashboard performance
- Fix some issues
- Fix unselected proxy group delay issues
- Fix asn url issues
- Update changelog
## v0.8.79
- Fix tab delay view issues
- Fix tray action issues
- Fix get profile redirect client ua issues
- Fix proxy card delay view issues
- Add Russian, Japanese adaptation
- Fix some issues
- Update changelog
## v0.8.78
- Fix list form input view issues
- Fix traffic view issues
- Update changelog
## v0.8.77
- Optimize performance
- Update core
- Optimize core stability
- Fix linux tun authority check error
- Fix some issues
- Fix scroll physics error
- Update changelog
## v0.8.75
- Add windows storage corruption detection
- Fix core crash caused by windows resource manager restart
- Optimize logs, requests, access to pages
- Fix macos bypass domain issues
- Update changelog
## v0.8.74
- Fix some issues

View File

@@ -1,8 +0,0 @@
android_arm64:
dart ./setup.dart android --arch arm64
macos_arm64:
dart ./setup.dart macos --arch arm64
android_arm64_core:
dart ./setup.dart android --arch arm64 --out core
macos_arm64_core:
dart ./setup.dart macos --arch arm64 --out core

View File

@@ -1 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -1,3 +1,5 @@
import com.android.build.gradle.tasks.MergeSourceSetFolders
plugins {
id "com.android.application"
id "kotlin-android"
@@ -31,7 +33,8 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
android {
namespace "com.follow.clash"
compileSdkVersion 35
compileSdkVersion 34
ndkVersion "27.1.12297006"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -45,7 +48,6 @@ android {
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
signingConfigs {
if (isRelease) {
release {
@@ -61,7 +63,7 @@ android {
defaultConfig {
applicationId "com.follow.clash"
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -82,15 +84,31 @@ android {
}
}
tasks.register('copyNativeLibs', Copy) {
delete('src/main/jniLibs')
from('../../libclash/android')
into('src/main/jniLibs')
}
tasks.withType(MergeSourceSetFolders).configureEach {
dependsOn copyNativeLibs
}
flutter {
source '../..'
}
dependencies {
implementation project(":core")
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'com.google.code.gson:gson:2.10.1'
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
implementation 'com.google.code.gson:gson:2.10'
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
exclude group: "com.google.guava", module: "guava"
}
}
}
afterEvaluate {
assembleDebug.dependsOn copyNativeLibs
assembleRelease.dependsOn copyNativeLibs
}

View File

@@ -1,17 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<application
android:icon="@mipmap/ic_launcher"
android:label="FlClash Debug"
tools:replace="android:label">
<application android:label="FlClash Debug" tools:replace="android:label">
<service
android:name=".services.FlClashTileService"
android:label="FlClash Debug"
tools:replace="android:label"
tools:targetApi="24" />
android:name=".services.FlClashTileService"
android:label="FlClash Debug"
tools:replace="android:label">
</service>
</application>
</manifest>

View File

@@ -10,12 +10,14 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
@@ -62,9 +64,7 @@
</intent-filter>
</activity>
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
<activity
android:name=".TempActivity"
@@ -87,6 +87,7 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -124,7 +125,7 @@
<service
android:name=".services.FlClashVpnService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
@@ -137,7 +138,7 @@
<service
android:name=".services.FlClashService"
android:exported="false"
android:foregroundServiceType="dataSync">
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" />

View File

@@ -1,7 +1,7 @@
package com.follow.clash;
import android.app.Application
import android.content.Context
import android.content.Context;
class FlClashApplication : Application() {
companion object {

View File

@@ -1,7 +1,9 @@
package com.follow.clash
import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import io.flutter.FlutterInjector

View File

@@ -1,6 +1,5 @@
package com.follow.clash
import com.follow.clash.core.Core
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin

View File

@@ -7,7 +7,6 @@ enum class AccessControlMode {
}
data class AccessControl(
val enable: Boolean,
val mode: AccessControlMode,
val acceptList: List<String>,
val rejectList: List<String>,
@@ -18,7 +17,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
data class VpnOptions(
val enable: Boolean,
val port: Int,
val accessControl: AccessControl,
val accessControl: AccessControl?,
val allowBypass: Boolean,
val systemProxy: Boolean,
val bypassDomain: List<String>,

View File

@@ -291,18 +291,16 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun getPackages(): List<Package> {
val packageManager = FlClashApplication.getAppContext().packageManager
if (packages.isNotEmpty()) return packages
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
?.filter {
it.packageName != FlClashApplication.getAppContext().packageName && (
it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
)
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != FlClashApplication.getAppContext().packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
}?.map {
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo?.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
lastUpdateTime = it.lastUpdateTime
)
}?.let { packages.addAll(it) }
@@ -355,7 +353,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
suspend fun getText(text: String): String? {
return withContext(Dispatchers.Default) {
return withContext(Dispatchers.Default){
channel.awaitResult<String>("getText", text)
}
}
@@ -393,33 +391,31 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}.forEach {
if (it.name.matches(chinaAppRegex)) return true
}
packageInfo.applicationInfo?.publicSourceDir?.let {
ZipFile(File(it)).use {
for (packageEntry in it.entries()) {
if (packageEntry.name.startsWith("firebase-")) return false
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
for (packageEntry in it.entries()) {
if (packageEntry.name.startsWith("firebase-")) return false
}
for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
) {
continue
}
for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
) {
continue
}
if (packageEntry.size > 15000000) {
return true
}
val input = it.getInputStream(packageEntry).buffered()
val dexFile = try {
DexBackedDexFile.fromInputStream(null, input)
} catch (e: Exception) {
return false
}
for (clazz in dexFile.classes) {
val clazzName =
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
.replace("$", ".")
if (clazzName.matches(chinaAppRegex)) return true
}
if (packageEntry.size > 15000000) {
return true
}
val input = it.getInputStream(packageEntry).buffered()
val dexFile = try {
DexBackedDexFile.fromInputStream(null, input)
} catch (e: Exception) {
return false
}
for (clazz in dexFile.classes) {
val clazzName =
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
.replace("$", ".")
if (clazzName.matches(chinaAppRegex)) return true
}
}
}

View File

@@ -1,5 +1,6 @@
package com.follow.clash.plugins
import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState
import com.follow.clash.models.VpnOptions
import com.google.gson.Gson
@@ -52,6 +53,7 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.handleStop()
GlobalState.destroyServiceEngine()
}
}

View File

@@ -14,9 +14,10 @@ import androidx.core.content.getSystemService
import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.core.Core
import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.getProtocol
import com.follow.clash.extensions.resolveDns
import com.follow.clash.models.Process
import com.follow.clash.models.StartForegroundParams
import com.follow.clash.models.VpnOptions
import com.follow.clash.services.BaseServiceInterface
@@ -39,12 +40,10 @@ import kotlin.concurrent.withLock
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel
private var flClashService: BaseServiceInterface? = null
private var options: VpnOptions? = null
private var isBind: Boolean = false
private lateinit var options: VpnOptions
private lateinit var scope: CoroutineScope
private var lastStartForegroundParams: StartForegroundParams? = null
private var timerJob: Job? = null
private val uidPageNameMap = mutableMapOf<Int, String>()
private val connectivity by lazy {
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
@@ -52,7 +51,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
isBind = true
flClashService = when (service) {
is FlClashVpnService.LocalBinder -> service.getService()
is FlClashService.LocalBinder -> service.getService()
@@ -62,7 +60,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
override fun onServiceDisconnected(arg: ComponentName) {
isBind = false
flClashService = null
}
}
@@ -93,6 +90,60 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.success(true)
}
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
if (flClashService is FlClashVpnService) {
(flClashService as FlClashVpnService).protect(fd)
}
result.success(true)
} else {
result.success(false)
}
}
"resolverProcess" -> {
val data = call.argument<String>("data")
val process = if (data != null) Gson().fromJson(
data, Process::class.java
) else null
val metadata = process?.metadata
if (metadata == null) {
result.success(null)
return
}
val protocol = metadata.getProtocol()
if (protocol == null) {
result.success(null)
return
}
scope.launch {
withContext(Dispatchers.Default) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.success(null)
return@withContext
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
metadata.destinationIP.ifEmpty { metadata.host },
metadata.destinationPort
)
val uid = try {
connectivity?.getConnectionOwnerUid(protocol, src, dst)
} catch (_: Exception) {
null
}
if (uid == null || uid == -1) {
result.success(null)
return@withContext
}
val packages =
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
}
else -> {
result.notImplemented()
}
@@ -100,9 +151,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
fun handleStart(options: VpnOptions): Boolean {
if (options.enable != this.options?.enable) {
this.flClashService = null
}
this.options = options
when (options.enable) {
true -> handleStartVpn()
@@ -112,9 +160,10 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
handleStartService()
}
GlobalState.getCurrentAppPlugin()
?.requestVpnPermission {
handleStartService()
}
}
fun requestGc() {
@@ -184,7 +233,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
private fun startForegroundJob() {
stopForegroundJob()
timerJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) {
startForeground()
@@ -206,58 +254,26 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
val fd = flClashService?.start(options!!)
Core.startTun(
fd = fd ?: 0,
protect = this::protect,
resolverProcess = this::resolverProcess,
val fd = flClashService?.start(options)
flutterMethodChannel.invokeMethod(
"started", fd
)
startForegroundJob()
startForegroundJob();
}
}
private fun protect(fd: Int): Boolean {
return (flClashService as? FlClashVpnService)?.protect(fd) == true
}
private fun resolverProcess(
protocol: Int,
source: InetSocketAddress,
target: InetSocketAddress,
uid: Int,
): String {
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
} else {
uid
}
if (nextUid == -1) {
return ""
}
if (!uidPageNameMap.containsKey(nextUid)) {
uidPageNameMap[nextUid] =
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(nextUid)
?.first() ?: ""
}
return uidPageNameMap[nextUid] ?: ""
}
fun handleStop() {
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
stopForegroundJob()
Core.stopTun()
flClashService?.stop()
GlobalState.handleTryDestroy()
}
}
private fun bindService() {
if (isBind) {
FlClashApplication.getAppContext().unbindService(connection)
}
val intent = when (options?.enable == true) {
val intent = when (options.enable) {
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
}

View File

@@ -7,7 +7,7 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Binder
import android.os.Build
import android.os.IBinder
@@ -87,7 +87,6 @@ class FlClashService : Service(), BaseServiceInterface {
}
}
}
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await()
}
@@ -101,8 +100,7 @@ class FlClashService : Service(), BaseServiceInterface {
}
}
@SuppressLint("ForegroundServiceType")
@SuppressLint("ForegroundServiceType", "WrongConstant")
override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
@@ -118,11 +116,7 @@ class FlClashService : Service(), BaseServiceInterface {
.setContentTitle(title)
.setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification)
}
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}

View File

@@ -6,7 +6,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Binder
@@ -45,16 +45,9 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute4",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("0.0.0.0", 0)
routeAddress.forEach { i ->
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}")
addRoute(i.address, i.prefixLength)
}
} else {
addRoute("0.0.0.0", 0)
@@ -65,16 +58,9 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
routeAddress.forEach { i ->
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}")
addRoute(i.address, i.prefixLength)
}
} else {
addRoute("::", 0)
@@ -82,19 +68,17 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
}
addDnsServer(options.dnsServerAddress)
setMtu(9000)
options.accessControl.let { accessControl ->
if (accessControl.enable) {
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
options.accessControl?.let { accessControl ->
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
}
AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
}
AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
}
}
}
@@ -179,7 +163,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
return notificationBuilderDeferred.await()
}
@SuppressLint("ForegroundServiceType")
@SuppressLint("ForegroundServiceType", "WrongConstant")
override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
@@ -196,11 +180,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
.setContentText(content)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification)
}
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}

View File

@@ -24,7 +24,6 @@ subprojects {
}
subprojects {
project.evaluationDependsOn(':app')
project.evaluationDependsOn(':core')
}
tasks.register("clean", Delete) {

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,72 +0,0 @@
import com.android.build.gradle.tasks.MergeSourceSetFolders
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.follow.clash.core"
compileSdk = 35
ndkVersion = "28.0.13004108"
defaultConfig {
minSdk = 21
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
sourceSets {
getByName("main") {
jniLibs.srcDirs("src/main/jniLibs")
}
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
tasks.register<Copy>("copyNativeLibs") {
doFirst {
delete("src/main/jniLibs")
}
from("../../libclash/android")
into("src/main/jniLibs")
}
tasks.withType<MergeSourceSetFolders>().configureEach {
dependsOn("copyNativeLibs")
}
afterEvaluate {
tasks.named("assembleDebug").configure {
dependsOn("copyNativeLibs")
}
tasks.named("assembleRelease").configure {
dependsOn("copyNativeLibs")
}
}
dependencies {
implementation("androidx.core:core-ktx:1.16.0")
}

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -1,41 +0,0 @@
cmake_minimum_required(VERSION 3.22.1)
project("core")
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
add_compile_options(-O3)
add_compile_options(-flto)
add_compile_options(-g0)
add_compile_options(-ffunction-sections -fdata-sections)
add_compile_options(-fno-exceptions -fno-rtti)
add_link_options(
-flto
-Wl,--gc-sections
-Wl,--strip-all
-Wl,--exclude-libs=ALL
)
endif ()
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")
if (EXISTS ${LIB_CLASH_PATH})
message(STATUS "Found libclash.so for ABI ${ANDROID_ABI}")
add_definitions(-D_LIBCLASH)
include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
clash)
else ()
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME})
endif ()

View File

@@ -1,88 +0,0 @@
#include <jni.h>
#ifdef _LIBCLASH
#include <jni.h>
#include <string>
#include "jni_helper.h"
#include "libclash.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
auto interface = new_global(cb);
startTUN(fd, interface);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
stopTun();
}
static jmethodID m_tun_interface_protect;
static jmethodID m_tun_interface_resolve_process;
static void release_jni_object_impl(void *obj) {
ATTACH_JNI();
del_global((jobject) obj);
}
static void call_tun_interface_protect_impl(void *tun_interface, int fd) {
ATTACH_JNI();
env->CallVoidMethod((jobject) tun_interface,
(jmethodID) m_tun_interface_protect,
(jint) fd);
}
static const char*
call_tun_interface_resolve_process_impl(void *tun_interface, int protocol,
const char *source,
const char *target,
int uid) {
ATTACH_JNI();
jstring packageName = (jstring)env->CallObjectMethod((jobject) tun_interface,
(jmethodID) m_tun_interface_resolve_process,
(jint) protocol,
(jstring) new_string(source),
(jstring) new_string(target),
(jint) uid);
return get_string(packageName);
}
extern "C"
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
initialize_jni(vm, env);
jclass c_tun_interface = find_class("com/follow/clash/core/TunInterface");
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V");
m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess",
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
registerCallbacks(&call_tun_interface_protect_impl,
&call_tun_interface_resolve_process_impl,
&release_jni_object_impl);
return JNI_VERSION_1_6;
}
#else
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
}
#endif

View File

@@ -1,70 +0,0 @@
#include "jni_helper.h"
#include <malloc.h>
#include <cstring>
static JavaVM *global_vm;
static jclass c_string;
static jmethodID m_new_string;
static jmethodID m_get_bytes;
void initialize_jni(JavaVM *vm, JNIEnv *env) {
global_vm = vm;
c_string = (jclass) new_global(find_class("java/lang/String"));
m_new_string = find_method(c_string, "<init>", "([B)V");
m_get_bytes = find_method(c_string, "getBytes", "()[B");
}
JavaVM *global_java_vm() {
return global_vm;
}
char *jni_get_string(JNIEnv *env, jstring str) {
auto array = (jbyteArray) env->CallObjectMethod(str, m_get_bytes);
int length = env->GetArrayLength(array);
char *content = (char *) malloc(length + 1);
env->GetByteArrayRegion(array, 0, length, (jbyte *) content);
content[length] = 0;
return content;
}
jstring jni_new_string(JNIEnv *env, const char *str) {
auto length = (int) strlen(str);
jbyteArray array = env->NewByteArray(length);
env->SetByteArrayRegion(array, 0, length, (const jbyte *) str);
return (jstring) env->NewObject(c_string, m_new_string, array);
}
int jni_catch_exception(JNIEnv *env) {
int result = env->ExceptionCheck();
if (result) {
env->ExceptionDescribe();
env->ExceptionClear();
}
return result;
}
void jni_attach_thread(struct scoped_jni *jni) {
JavaVM *vm = global_java_vm();
if (vm->GetEnv((void **) &jni->env, JNI_VERSION_1_6) == JNI_OK) {
jni->require_release = 0;
return;
}
if (vm->AttachCurrentThread(&jni->env, nullptr) != JNI_OK) {
abort();
}
jni->require_release = 1;
}
void jni_detach_thread(struct scoped_jni *jni) {
JavaVM *vm = global_java_vm();
if (jni->require_release) {
vm->DetachCurrentThread();
}
}
void release_string(char **str) {
free(*str);
}

View File

@@ -1,39 +0,0 @@
#pragma once
#include <jni.h>
#include <cstdint>
#include <cstdlib>
#include <malloc.h>
struct scoped_jni {
JNIEnv *env;
int require_release;
};
extern void initialize_jni(JavaVM *vm, JNIEnv *env);
extern jstring jni_new_string(JNIEnv *env, const char *str);
extern char *jni_get_string(JNIEnv *env, jstring str);
extern int jni_catch_exception(JNIEnv *env);
extern void jni_attach_thread(struct scoped_jni *jni);
extern void jni_detach_thread(struct scoped_jni *env);
extern void release_string(char **str);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
struct scoped_jni _jni; \
jni_attach_thread(&_jni); \
JNIEnv *env = _jni.env
#define scoped_string __attribute__((cleanup(release_string))) char*
#define find_class(name) env->FindClass(name)
#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature)
#define new_global(obj) env->NewGlobalRef(obj)
#define del_global(obj) env->DeleteGlobalRef(obj)
#define get_string(jstr) jni_get_string(env, jstr)
#define new_string(cstr) jni_new_string(env, cstr)

View File

@@ -1,50 +0,0 @@
package com.follow.clash.core
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.URL
data object Core {
private external fun startTun(
fd: Int,
cb: TunInterface
)
private fun parseInetSocketAddress(address: String): InetSocketAddress {
val url = URL("https://$address")
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
}
fun startTun(
fd: Int,
protect: (Int) -> Boolean,
resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String
) {
startTun(fd, object : TunInterface {
override fun protect(fd: Int) {
protect(fd)
}
override fun resolverProcess(
protocol: Int,
source: String,
target: String,
uid: Int
): String {
return resolverProcess(
protocol,
parseInetSocketAddress(source),
parseInetSocketAddress(target),
uid,
)
}
});
}
external fun stopTun()
init {
System.loadLibrary("core")
}
}

View File

@@ -1,9 +0,0 @@
package com.follow.clash.core
import androidx.annotation.Keep
@Keep
interface TunInterface {
fun protect(fd: Int)
fun resolverProcess(protocol: Int, source: String, target: String, uid: Int): String
}

View File

@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
kotlin_version=1.9.22
agp_version=8.9.1
agp_version=8.2.1

View File

@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip

View File

@@ -24,4 +24,3 @@ plugins {
}
include ":app"
include ':core'

Binary file not shown.

View File

@@ -5,7 +5,6 @@ targets:
options:
build_extensions:
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
freezed:
options:
build_extensions:

View File

@@ -22,99 +22,99 @@ func (result ActionResult) Json() ([]byte, error) {
return data, err
}
func (action Action) getResult(data interface{}) []byte {
resultAction := ActionResult{
func (action Action) wrapMessage(data interface{}) []byte {
sendAction := ActionResult{
Id: action.Id,
Method: action.Method,
Data: data,
}
res, _ := resultAction.Json()
res, _ := sendAction.Json()
return res
}
func handleAction(action *Action, result func(data interface{})) {
func handleAction(action *Action, send func([]byte)) {
switch action.Method {
case initClashMethod:
paramsString := action.Data.(string)
result(handleInitClash(paramsString))
data := action.Data.(string)
send(action.wrapMessage(handleInitClash(data)))
return
case getIsInitMethod:
result(handleGetIsInit())
send(action.wrapMessage(handleGetIsInit()))
return
case forceGcMethod:
handleForceGc()
result(true)
send(action.wrapMessage(true))
return
case shutdownMethod:
result(handleShutdown())
send(action.wrapMessage(handleShutdown()))
return
case validateConfigMethod:
data := []byte(action.Data.(string))
result(handleValidateConfig(data))
send(action.wrapMessage(handleValidateConfig(data)))
return
case updateConfigMethod:
data := []byte(action.Data.(string))
result(handleUpdateConfig(data))
send(action.wrapMessage(handleUpdateConfig(data)))
return
case getProxiesMethod:
result(handleGetProxies())
send(action.wrapMessage(handleGetProxies()))
return
case changeProxyMethod:
data := action.Data.(string)
handleChangeProxy(data, func(value string) {
result(value)
send(action.wrapMessage(value))
})
return
case getTrafficMethod:
result(handleGetTraffic())
send(action.wrapMessage(handleGetTraffic()))
return
case getTotalTrafficMethod:
result(handleGetTotalTraffic())
send(action.wrapMessage(handleGetTotalTraffic()))
return
case resetTrafficMethod:
handleResetTraffic()
result(true)
send(action.wrapMessage(true))
return
case asyncTestDelayMethod:
data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) {
result(value)
send(action.wrapMessage(value))
})
return
case getConnectionsMethod:
result(handleGetConnections())
send(action.wrapMessage(handleGetConnections()))
return
case closeConnectionsMethod:
result(handleCloseConnections())
send(action.wrapMessage(handleCloseConnections()))
return
case closeConnectionMethod:
id := action.Data.(string)
result(handleCloseConnection(id))
send(action.wrapMessage(handleCloseConnection(id)))
return
case getExternalProvidersMethod:
result(handleGetExternalProviders())
send(action.wrapMessage(handleGetExternalProviders()))
return
case getExternalProviderMethod:
externalProviderName := action.Data.(string)
result(handleGetExternalProvider(externalProviderName))
send(action.wrapMessage(handleGetExternalProvider(externalProviderName)))
case updateGeoDataMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
result(err.Error())
send(action.wrapMessage(err.Error()))
return
}
geoType := params["geo-type"]
geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) {
result(value)
send(action.wrapMessage(value))
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
result(value)
send(action.wrapMessage(value))
})
return
case sideLoadExternalProviderMethod:
@@ -122,56 +122,46 @@ func handleAction(action *Action, result func(data interface{})) {
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
result(err.Error())
send(action.wrapMessage(err.Error()))
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
result(value)
send(action.wrapMessage(value))
})
return
case startLogMethod:
handleStartLog()
result(true)
send(action.wrapMessage(true))
return
case stopLogMethod:
handleStopLog()
result(true)
send(action.wrapMessage(true))
return
case startListenerMethod:
result(handleStartListener())
send(action.wrapMessage(handleStartListener()))
return
case stopListenerMethod:
result(handleStopListener())
send(action.wrapMessage(handleStopListener()))
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
result(value)
send(action.wrapMessage(value))
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
result(value)
send(action.wrapMessage(value))
})
return
case getProfileMethod:
profileId := action.Data.(string)
handleGetMemory(func(value string) {
result(handleGetProfile(profileId))
})
return
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
result(true)
default:
handle := nextHandle(action, result)
handle := nextHandle(action, send)
if handle {
return
} else {
result(action.DefaultValue)
send(action.wrapMessage(action.DefaultValue))
}
}
}

View File

@@ -1,77 +0,0 @@
//go:build android && cgo
package main
/*
#include <stdlib.h>
typedef void (*release_object_func)(void *obj);
typedef void (*protect_func)(void *tun_interface, int fd);
typedef const char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid);
static void protect(protect_func fn, void *tun_interface, int fd) {
if (fn) {
fn(tun_interface, fd);
}
}
static const char* resolve_process(resolve_process_func fn, void *tun_interface, int protocol, const char *source, const char *target, int uid) {
if (fn) {
return fn(tun_interface, protocol, source, target, uid);
}
return "";
}
static void release_object(release_object_func fn, void *obj) {
if (fn) {
return fn(obj);
}
}
*/
import "C"
import (
"unsafe"
)
var (
globalCallbacks struct {
releaseObjectFunc C.release_object_func
protectFunc C.protect_func
resolveProcessFunc C.resolve_process_func
}
)
func protect(callback unsafe.Pointer, fd int) {
if globalCallbacks.protectFunc != nil {
C.protect(globalCallbacks.protectFunc, callback, C.int(fd))
}
}
func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
if globalCallbacks.resolveProcessFunc == nil {
return ""
}
s := C.CString(source)
defer C.free(unsafe.Pointer(s))
t := C.CString(target)
defer C.free(unsafe.Pointer(t))
res := C.resolve_process(globalCallbacks.resolveProcessFunc, callback, C.int(protocol), s, t, C.int(uid))
defer C.free(unsafe.Pointer(res))
return C.GoString(res)
}
func releaseObject(callback unsafe.Pointer) {
if globalCallbacks.releaseObjectFunc == nil {
return
}
C.release_object(globalCallbacks.releaseObjectFunc, callback)
}
//export registerCallbacks
func registerCallbacks(markSocketFunc C.protect_func, resolveProcessFunc C.resolve_process_func, releaseObjectFunc C.release_object_func) {
globalCallbacks.protectFunc = markSocketFunc
globalCallbacks.resolveProcessFunc = resolveProcessFunc
globalCallbacks.releaseObjectFunc = releaseObjectFunc
}

View File

@@ -28,23 +28,10 @@ import (
"sync"
)
func splitByMultipleSeparators(s string) interface{} {
isSeparator := func(r rune) bool {
return r == ',' || r == ' ' || r == ';'
}
parts := strings.FieldsFunc(s, isSeparator)
if len(parts) > 1 {
return parts
}
return s
}
var (
version = 0
isRunning = false
runLock sync.Mutex
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
)
@@ -173,19 +160,9 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
return prof
}
func attachHosts(hosts, patchHosts map[string]any) {
func genHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts {
if str, ok := v.(string); ok {
hosts[k] = splitByMultipleSeparators(str)
}
}
}
func updatePatchDns(dns config.RawDNS) {
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
if str, ok := pair.Value.(string); ok {
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
}
hosts[k] = v
}
}
@@ -196,25 +173,26 @@ func trimArr(arr []string) (r []string) {
return
}
func overrideRules(rules, patchRules []string) []string {
target := ""
for _, line := range rules {
func overrideRules(rules *[]string) {
var target = ""
for _, line := range *rules {
rule := trimArr(strings.Split(line, ","))
if len(rule) != 2 {
continue
l := len(rule)
if l != 2 {
return
}
if strings.EqualFold(rule[0], "MATCH") {
if strings.ToUpper(rule[0]) == "MATCH" {
target = rule[1]
break
}
}
if target == "" {
return rules
return
}
rulesExt := lo.Map(ips, func(ip string, _ int) string {
return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
var rulesExt = lo.Map(ips, func(ip string, index int) string {
return fmt.Sprintf("DOMAIN %s %s", ip, target)
})
return append(append(rulesExt, patchRules...), rules...)
*rules = append(rulesExt, *rules...)
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
@@ -237,7 +215,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl
@@ -248,20 +225,15 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = ""
}
attachHosts(targetConfig.Hosts, patchConfig.Hosts)
genHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
updatePatchDns(patchConfig.DNS)
targetConfig.DNS = patchConfig.DNS
} else {
if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true
}
}
if configParams.OverrideRule {
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
} else {
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
}
overrideRules(&targetConfig.Rule)
}
func patchConfig() {

View File

@@ -7,18 +7,13 @@ import (
"time"
)
type InitParams struct {
HomeDir string `json:"home-dir"`
Version int `json:"version"`
}
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"`
OverrideDns bool `json:"override-dns"`
OverrideRule bool `json:"override-rule"`
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
}
type GenerateConfigParams struct {
@@ -76,12 +71,15 @@ const (
stopLogMethod Method = "stopLog"
startListenerMethod Method = "startListener"
stopListenerMethod Method = "stopListener"
startTunMethod Method = "startTun"
stopTunMethod Method = "stopTun"
updateDnsMethod Method = "updateDns"
setProcessMapMethod Method = "setProcessMap"
setFdMapMethod Method = "setFdMap"
setStateMethod Method = "setState"
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName"
getProfileMethod Method = "getProfile"
)
type Method string
@@ -110,3 +108,20 @@ func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
type InvokeMessage struct {
Type InvokeType `json:"type"`
Data interface{} `json:"data"`
}
type InvokeType string
const (
ProtectInvoke InvokeType = "protect"
ProcessInvoke InvokeType = "process"
)
func (message *InvokeMessage) Json() string {
data, _ := json.Marshal(message)
return string(data)
}

View File

@@ -6,7 +6,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.49.1
github.com/samber/lo v1.47.0
)
require (
@@ -19,28 +19,28 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/enfein/mieru/v3 v3.10.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-chi/chi/v5 v5.2.0 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.1 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
@@ -50,24 +50,22 @@ require (
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.19.0 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.1 // indirect
github.com/metacubex/chacha v0.1.0 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
github.com/metacubex/sing-tun v0.4.5 // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
github.com/metacubex/utls v1.6.8-alpha.4 // indirect
github.com/metacubex/utls v1.6.6 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -75,18 +73,18 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.2 // indirect
github.com/sagernet/sing v0.5.1 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/shirou/gopsutil/v4 v4.24.11 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
@@ -103,13 +101,13 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect

View File

@@ -24,12 +24,12 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -43,8 +43,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -74,8 +74,8 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -84,7 +84,6 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -97,45 +96,40 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI=
github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
@@ -155,8 +149,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
@@ -170,18 +164,18 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJ
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
@@ -224,8 +218,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -234,11 +228,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -254,12 +248,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -269,8 +263,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,7 +2,6 @@ package main
import (
"context"
"core/state"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/adapter"
@@ -27,22 +26,18 @@ import (
)
var (
isInit = false
configParams = ConfigExtendedParams{}
isInit = false
configParams = ConfigExtendedParams{
OnlyStatisticsProxy: false,
}
externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config
)
func handleInitClash(paramsString string) bool {
var params = InitParams{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
return false
}
version = params.Version
func handleInitClash(homeDirStr string) bool {
if !isInit {
constant.SetHomeDir(params.HomeDir)
constant.SetHomeDir(homeDirStr)
isInit = true
}
return isInit
@@ -157,7 +152,7 @@ func handleChangeProxy(data string, fn func(string string)) {
}
func handleGetTraffic() string {
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -171,7 +166,7 @@ func handleGetTraffic() string {
}
func handleGetTotalTraffic() string {
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -184,15 +179,6 @@ func handleGetTotalTraffic() string {
return string(data)
}
func handleGetProfile(profileId string) string {
prof := getRawConfigWithId(profileId)
data, err := json.Marshal(prof)
if err != nil {
return ""
}
return string(data)
}
func handleResetTraffic() {
statistic.DefaultManager.ResetStatistic()
}
@@ -234,7 +220,6 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
if params.TestUrl != "" {
testUrl = params.TestUrl
}
delayData.Url = testUrl
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
if err != nil || delay == 0 {
@@ -438,10 +423,6 @@ func handleGetMemory(fn func(value string)) {
}()
}
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{

View File

@@ -49,8 +49,8 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
bridge.SendToPort(i, err.Error())
return
}
go handleAction(action, func(data interface{}) {
bridge.SendToPort(i, string(action.getResult(data)))
go handleAction(action, func(bytes []byte) {
bridge.SendToPort(i, string(bytes))
})
}
@@ -64,7 +64,7 @@ func sendMessage(message Message) {
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod,
}.getResult(res)))
}.wrapMessage(res)))
}
//export startListener

View File

@@ -4,7 +4,6 @@ package main
import "C"
import (
"context"
bridge "core/dart-bridge"
"core/platform"
"core/state"
@@ -12,116 +11,128 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"net"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unsafe"
)
type TunHandler struct {
listener *sing_tun.Listener
callback unsafe.Pointer
limit *semaphore.Weighted
type Fd struct {
Id string `json:"id"`
Value int64 `json:"value"`
}
func (t *TunHandler) close() {
_ = t.limit.Acquire(context.TODO(), 4)
defer t.limit.Release(4)
removeTunHook()
if t.listener != nil {
_ = t.listener.Close()
}
if t.callback != nil {
releaseObject(t.callback)
}
t.callback = nil
t.listener = nil
type Process struct {
Id string `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
func (t *TunHandler) handleProtect(fd int) {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
type ProcessMapItem struct {
Id string `json:"id"`
Value string `json:"value"`
}
if t.listener == nil {
type InvokeManager struct {
invokeMap sync.Map
chanMap map[string]chan struct{}
chanLock sync.Mutex
}
func NewInvokeManager() *InvokeManager {
return &InvokeManager{
chanMap: make(map[string]chan struct{}),
}
}
func (m *InvokeManager) load(id string) string {
res, ok := m.invokeMap.Load(id)
if ok {
return res.(string)
}
return ""
}
func (m *InvokeManager) delete(id string) {
m.invokeMap.Delete(id)
}
func (m *InvokeManager) completer(id string, value string) {
m.invokeMap.Store(id, value)
m.chanLock.Lock()
if ch, ok := m.chanMap[id]; ok {
close(ch)
delete(m.chanMap, id)
}
m.chanLock.Unlock()
}
func (m *InvokeManager) await(id string) {
m.chanLock.Lock()
if _, ok := m.chanMap[id]; !ok {
m.chanMap[id] = make(chan struct{})
}
ch := m.chanMap[id]
m.chanLock.Unlock()
timeout := time.After(500 * time.Millisecond)
select {
case <-ch:
return
case <-timeout:
m.completer(id, "")
return
}
protect(t.callback, fd)
}
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.listener == nil {
return ""
}
var protocol int
uid := -1
switch source.Network() {
case "udp", "udp4", "udp6":
protocol = syscall.IPPROTO_UDP
case "tcp", "tcp4", "tcp6":
protocol = syscall.IPPROTO_TCP
}
if version < 29 {
uid = platform.QuerySocketUidFromProcFs(source, target)
}
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
}
var (
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
tunHandler *TunHandler
invokePort int64 = -1
tunListener *sing_tun.Listener
fdInvokeMap = NewInvokeManager()
processInvokeMap = NewInvokeManager()
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
)
func handleStartTun(fd int) string {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
if fd == 0 {
now := time.Now()
runTime = &now
} else {
initSocketHook()
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
}
now := time.Now()
runTime = &now
}
return handleGetRunTime()
}
func handleStopTun() {
tunLock.Lock()
defer tunLock.Unlock()
removeSocketHook()
runTime = nil
if tunHandler != nil {
tunHandler.close()
if tunListener != nil {
log.Infoln("TUN close")
_ = tunListener.Close()
}
}
func handleStartTun(fd int, callback unsafe.Pointer) bool {
handleStopTun()
now := time.Now()
runTime = &now
if fd != 0 {
tunLock.Lock()
defer tunLock.Unlock()
tunHandler = &TunHandler{
callback: callback,
limit: semaphore.NewWeighted(4),
}
initTunHook()
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
} else {
removeTunHook()
return false
}
tunHandler.listener = tunListener
}
return true
}
func handleGetRunTime() string {
if runTime == nil {
return ""
@@ -129,41 +140,99 @@ func handleGetRunTime() string {
return strconv.FormatInt(runTime.UnixMilli(), 10)
}
func initTunHook() {
func handleSetProcessMap(params string) {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(params), processMapItem)
if err == nil {
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
}
}
//export attachInvokePort
func attachInvokePort(mPort C.longlong) {
invokePort = int64(mPort)
}
func sendInvokeMessage(message InvokeMessage) {
if invokePort == -1 {
return
}
bridge.SendToPort(invokePort, message.Json())
}
func handleMarkSocket(fd Fd) {
sendInvokeMessage(InvokeMessage{
Type: ProtectInvoke,
Data: fd,
})
}
func handleParseProcess(process Process) {
sendInvokeMessage(InvokeMessage{
Type: ProcessInvoke,
Data: process,
})
}
func handleSetFdMap(id string) {
go func() {
fdInvokeMap.completer(id, "")
}()
}
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
tunHandler.handleProtect(int(fd))
fdInt := int64(fd)
id := utils.NewUUIDV1().String()
handleMarkSocket(Fd{
Id: id,
Value: fdInt,
})
fdInvokeMap.await(id)
fdInvokeMap.delete(id)
})
}
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork
}
return tunHandler.handleResolveProcess(src, dst), nil
}
}
func removeTunHook() {
func removeSocketHook() {
dialer.DefaultSocketHook = nil
process.DefaultPackageNameResolver = nil
}
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil {
return "", process.ErrInvalidNetwork
}
id := utils.NewUUIDV1().String()
handleParseProcess(Process{
Id: id,
Metadata: metadata,
})
processInvokeMap.await(id)
res := processInvokeMap.load(id)
processInvokeMap.delete(id)
return res, nil
}
}
func handleGetAndroidVpnOptions() string {
tunLock.Lock()
defer tunLock.Unlock()
options := state.AndroidVpnOptions{
Enable: state.CurrentState.VpnProps.Enable,
Enable: state.CurrentState.Enable,
Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.VpnProps.AccessControl,
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
RouteAddress: currentConfig.General.Tun.RouteAddress,
AccessControl: state.CurrentState.AccessControl,
SystemProxy: state.CurrentState.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass,
RouteAddress: state.CurrentState.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
@@ -175,6 +244,10 @@ func handleGetAndroidVpnOptions() string {
return string(data)
}
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func handleUpdateDns(value string) {
go func() {
log.Infoln("[DNS] updateDns %s", value)
@@ -190,34 +263,59 @@ func handleGetCurrentProfileName() string {
return state.CurrentState.CurrentProfileName
}
func nextHandle(action *Action, result func(data interface{})) bool {
func nextHandle(action *Action, send func([]byte)) bool {
switch action.Method {
case startTunMethod:
data := action.Data.(string)
var fd int
_ = json.Unmarshal([]byte(data), &fd)
send(action.wrapMessage(handleStartTun(fd)))
return true
case stopTunMethod:
handleStopTun()
send(action.wrapMessage(true))
return true
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
send(action.wrapMessage(true))
return true
case getAndroidVpnOptionsMethod:
result(handleGetAndroidVpnOptions())
send(action.wrapMessage(handleGetAndroidVpnOptions()))
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
result(true)
send(action.wrapMessage(true))
return true
case setFdMapMethod:
fdId := action.Data.(string)
handleSetFdMap(fdId)
send(action.wrapMessage(true))
return true
case setProcessMapMethod:
data := action.Data.(string)
handleSetProcessMap(data)
send(action.wrapMessage(true))
return true
case getRunTimeMethod:
result(handleGetRunTime())
send(action.wrapMessage(handleGetRunTime()))
return true
case getCurrentProfileNameMethod:
result(handleGetCurrentProfileName())
send(action.wrapMessage(handleGetCurrentProfileName()))
return true
}
return false
}
//export quickStart
func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(initParamsChar)
dir := C.GoString(dirChar)
bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar)
go func() {
res := handleInitClash(paramsString)
res := handleInitClash(dir)
if res == false {
bridge.SendToPort(i, "init error")
}
@@ -227,8 +325,9 @@ func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.c
}
//export startTUN
func startTUN(fd C.int, callback unsafe.Pointer) bool {
return handleStartTun(int(fd), callback)
func startTUN(fd C.int) *C.char {
f := int(fd)
return C.CString(handleStartTun(f))
}
//export getRunTime
@@ -241,6 +340,12 @@ func stopTun() {
handleStopTun()
}
//export setFdMap
func setFdMap(fdIdChar *C.char) {
fdId := C.GoString(fdIdChar)
handleSetFdMap(fdId)
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
return C.CString(handleGetCurrentProfileName())
@@ -262,3 +367,12 @@ func updateDns(s *C.char) {
dnsList := C.GoString(s)
handleUpdateDns(dnsList)
}
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
handleSetProcessMap(paramsString)
}

View File

@@ -2,6 +2,10 @@
package main
func nextHandle(action *Action, result func(data interface{})) bool {
func nextHandle(action *Action) {
return action
}
func nextHandle(action *Action, send func([]byte)) bool {
return false
}

View File

@@ -1,176 +0,0 @@
//go:build linux
// +build linux
package platform
import (
"bufio"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"os"
"strconv"
"strings"
"unsafe"
)
var netIndexOfLocal = -1
var netIndexOfUid = -1
var nativeEndian binary.ByteOrder
func QuerySocketUidFromProcFs(source, _ net.Addr) int {
if netIndexOfLocal < 0 || netIndexOfUid < 0 {
return -1
}
network := source.Network()
if strings.HasSuffix(network, "4") || strings.HasSuffix(network, "6") {
network = network[:len(network)-1]
}
path := "/proc/net/" + network
var sIP net.IP
var sPort int
switch s := source.(type) {
case *net.TCPAddr:
sIP = s.IP
sPort = s.Port
case *net.UDPAddr:
sIP = s.IP
sPort = s.Port
default:
return -1
}
sIP = sIP.To16()
if sIP == nil {
return -1
}
uid := doQuery(path+"6", sIP, sPort)
if uid == -1 {
sIP = sIP.To4()
if sIP == nil {
return -1
}
uid = doQuery(path, sIP, sPort)
}
return uid
}
func doQuery(path string, sIP net.IP, sPort int) int {
file, err := os.Open(path)
if err != nil {
return -1
}
defer func(file *os.File) {
_ = file.Close()
}(file)
reader := bufio.NewReader(file)
var bytes [2]byte
binary.BigEndian.PutUint16(bytes[:], uint16(sPort))
local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:]))
for {
row, _, err := reader.ReadLine()
if err != nil {
return -1
}
fields := strings.Fields(string(row))
if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid {
continue
}
if strings.EqualFold(local, fields[netIndexOfLocal]) {
uid, err := strconv.Atoi(fields[netIndexOfUid])
if err != nil {
return -1
}
return uid
}
}
}
func nativeEndianIP(ip net.IP) []byte {
result := make([]byte, len(ip))
for i := 0; i < len(ip); i += 4 {
value := binary.BigEndian.Uint32(ip[i:])
nativeEndian.PutUint32(result[i:], value)
}
return result
}
func init() {
file, err := os.Open("/proc/net/tcp")
if err != nil {
return
}
defer func(file *os.File) {
_ = file.Close()
}(file)
reader := bufio.NewReader(file)
header, _, err := reader.ReadLine()
if err != nil {
return
}
columns := strings.Fields(string(header))
var txQueue, rxQueue, tr, tmWhen bool
for idx, col := range columns {
offset := 0
if txQueue && rxQueue {
offset--
}
if tr && tmWhen {
offset--
}
switch col {
case "tx_queue":
txQueue = true
case "rx_queue":
rxQueue = true
case "tr":
tr = true
case "tm->when":
tmWhen = true
case "local_address":
netIndexOfLocal = idx + offset
case "uid":
netIndexOfUid = idx + offset
}
}
}
func init() {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}

View File

@@ -19,7 +19,7 @@ func sendMessage(message Message) {
}
send(Action{
Method: messageMethod,
}.getResult(res))
}.wrapMessage(res))
}
func send(data []byte) {
@@ -61,12 +61,12 @@ func startServer(arg string) {
return
}
go handleAction(action, func(data interface{}) {
send(action.getResult(data))
go handleAction(action, func(bytes []byte) {
send(bytes)
})
}
}
func nextHandle(action *Action, result func(data interface{})) bool {
func nextHandle(action *Action, send func([]byte)) bool {
return false
}

View File

@@ -1,6 +1,6 @@
package state
//go:build android && cgo
import "net/netip"
package state
var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2"
@@ -13,14 +13,13 @@ type AndroidVpnOptions struct {
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"`
RouteAddress []netip.Prefix `json:"routeAddress"`
RouteAddress []string `json:"routeAddress"`
Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"`
}
type AccessControl struct {
Enable bool `json:"enable"`
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
@@ -32,23 +31,20 @@ type AndroidVpnRawOptions struct {
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
RouteAddress []string `json:"routeAddress"`
Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
}
type State struct {
VpnProps AndroidVpnRawOptions `json:"vpn-props"`
CurrentProfileName string `json:"current-profile-name"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
BypassDomain []string `json:"bypass-domain"`
AndroidVpnRawOptions
CurrentProfileName string `json:"currentProfileName"`
}
var CurrentState = &State{
OnlyStatisticsProxy: false,
CurrentProfileName: "",
}
var CurrentState = &State{}
func GetIpv6Address() string {
if CurrentState.VpnProps.Ipv6 {
if CurrentState.Ipv6 {
return DefaultIpv6Address
} else {
return ""

View File

@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
}
prefix4 = append(prefix4, tempPrefix4)
var prefix6 []netip.Prefix
if state.CurrentState.VpnProps.Ipv6 {
if state.CurrentState.Ipv6 {
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
if err != nil {
log.Errorln("startTUN error:", err)

View File

@@ -7,27 +7,57 @@ import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
import 'controller.dart';
import 'models/models.dart';
import 'pages/pages.dart';
class Application extends ConsumerStatefulWidget {
runAppWithPreferences(
Widget child, {
required AppState appState,
required Config config,
required AppFlowingState appFlowingState,
required ClashConfig clashConfig,
}) {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<ClashConfig>(
create: (_) => clashConfig,
),
ChangeNotifierProvider<Config>(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => appFlowingState,
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode;
appState?.selectedMap = config.currentSelectedMap;
return appState!;
},
)
],
child: child,
));
}
class Application extends StatefulWidget {
const Application({
super.key,
});
@override
ConsumerState<Application> createState() => ApplicationState();
State<Application> createState() => ApplicationState();
}
class ApplicationState extends ConsumerState<Application> {
late ColorSchemes systemColorSchemes;
class ApplicationState extends State<Application> {
late SystemColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer;
@@ -43,7 +73,7 @@ class ApplicationState extends ConsumerState<Application> {
ColorScheme _getAppColorScheme({
required Brightness brightness,
int? primaryColor,
required ColorSchemes systemColorSchemes,
required SystemColorSchemes systemColorSchemes,
}) {
if (primaryColor != null) {
return ColorScheme.fromSeed(
@@ -51,7 +81,7 @@ class ApplicationState extends ConsumerState<Application> {
brightness: brightness,
);
} else {
return systemColorSchemes.getColorSchemeForBrightness(brightness);
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
}
}
@@ -60,11 +90,12 @@ class ApplicationState extends ConsumerState<Application> {
super.initState();
_autoUpdateGroupTask();
_autoUpdateProfilesTask();
globalState.appController = AppController(context, ref);
globalState.appController = AppController(context);
globalState.measure = Measure.of(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) {
globalState.appController = AppController(currentContext, ref);
globalState.appController = AppController(currentContext);
}
await globalState.appController.init();
globalState.appController.initLink();
@@ -88,7 +119,7 @@ class ApplicationState extends ConsumerState<Application> {
});
}
_buildPlatformState(Widget child) {
_buildPlatformWrap(Widget child) {
if (system.isDesktop) {
return WindowManager(
child: TrayManager(
@@ -107,7 +138,18 @@ class ApplicationState extends ConsumerState<Application> {
);
}
_buildState(Widget child) {
_buildPage(Widget page) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: page,
);
}
return VpnManager(
child: page,
);
}
_buildWrap(Widget child) {
return AppStateManager(
child: ClashManager(
child: ConnectivityManager(
@@ -121,30 +163,11 @@ class ApplicationState extends ConsumerState<Application> {
);
}
_buildPlatformApp(Widget child) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: child,
);
}
return VpnManager(
child: child,
);
}
_buildApp(Widget child) {
return MessageManager(
child: ThemeManager(
child: child,
),
);
}
_updateSystemColorSchemes(
ColorScheme? lightDynamic,
ColorScheme? darkDynamic,
) {
systemColorSchemes = ColorSchemes(
systemColorSchemes = SystemColorSchemes(
lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic,
);
@@ -155,18 +178,21 @@ class ApplicationState extends ConsumerState<Application> {
@override
Widget build(context) {
return _buildPlatformState(
_buildState(
Consumer(
builder: (_, ref, child) {
final locale =
ref.watch(appSettingProvider.select((state) => state.locale));
final themeProps = ref.watch(themeSettingProvider);
return _buildPlatformWrap(
_buildWrap(
Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.appSetting.locale,
themeMode: config.themeProps.themeMode,
primaryColor: config.themeProps.primaryColor,
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
),
builder: (_, state, child) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
@@ -175,34 +201,43 @@ class ApplicationState extends ConsumerState<Application> {
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return AppEnvManager(
child: _buildPlatformApp(
_buildApp(child!),
return MessageManager(
child: LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
return _buildPage(child!);
},
),
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(locale),
locale: other.getLocaleForString(state.locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: themeProps.themeMode,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: themeProps.primaryColor,
primaryColor: state.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: themeProps.primaryColor,
).toPureBlack(themeProps.pureBlack),
primaryColor: state.primaryColor,
).toPrueBlack(state.prueBlack),
),
home: child,
);

View File

@@ -8,7 +8,6 @@ import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
@@ -64,19 +63,13 @@ class ClashCore {
}
}
Future<bool> init() async {
Future<bool> init({
required ClashConfig clashConfig,
required Config config,
}) async {
await initGeo();
final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(
InitParams(
homeDir: homeDirPath,
version: globalState.appState.version,
),
);
}
Future<bool> setState(CoreState state) async {
return await clashInterface.setState(state);
return await clashInterface.init(homeDirPath);
}
shutdown() async {
@@ -241,14 +234,6 @@ class ClashCore {
return int.parse(value);
}
Future<ClashConfigSnippet?> getProfile(String id) async {
final res = await clashInterface.getProfile(id);
if (res.isEmpty) {
return null;
}
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
}
resetTraffic() {
clashInterface.resetTraffic();
}

View File

@@ -2348,97 +2348,6 @@ class ClashFFI {
set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value;
void protect(
protect_func fn,
ffi.Pointer<ffi.Void> tun_interface,
int fd,
) {
return _protect(
fn,
tun_interface,
fd,
);
}
late final _protectPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
protect_func, ffi.Pointer<ffi.Void>, ffi.Int)>>('protect');
late final _protect = _protectPtr
.asFunction<void Function(protect_func, ffi.Pointer<ffi.Void>, int)>();
ffi.Pointer<ffi.Char> resolve_process(
resolve_process_func fn,
ffi.Pointer<ffi.Void> tun_interface,
int protocol,
ffi.Pointer<ffi.Char> source,
ffi.Pointer<ffi.Char> target,
int uid,
) {
return _resolve_process(
fn,
tun_interface,
protocol,
source,
target,
uid,
);
}
late final _resolve_processPtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(
resolve_process_func,
ffi.Pointer<ffi.Void>,
ffi.Int,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Int)>>('resolve_process');
late final _resolve_process = _resolve_processPtr.asFunction<
ffi.Pointer<ffi.Char> Function(
resolve_process_func,
ffi.Pointer<ffi.Void>,
int,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
int)>();
void release_object(
release_object_func fn,
ffi.Pointer<ffi.Void> obj,
) {
return _release_object(
fn,
obj,
);
}
late final _release_objectPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
release_object_func, ffi.Pointer<ffi.Void>)>>('release_object');
late final _release_object = _release_objectPtr
.asFunction<void Function(release_object_func, ffi.Pointer<ffi.Void>)>();
void registerCallbacks(
protect_func markSocketFunc,
resolve_process_func resolveProcessFunc,
release_object_func releaseObjectFunc,
) {
return _registerCallbacks(
markSocketFunc,
resolveProcessFunc,
releaseObjectFunc,
);
}
late final _registerCallbacksPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(protect_func, resolve_process_func,
release_object_func)>>('registerCallbacks');
late final _registerCallbacks = _registerCallbacksPtr.asFunction<
void Function(protect_func, resolve_process_func, release_object_func)>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
) {
@@ -2534,14 +2443,28 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
void attachInvokePort(
int mPort,
) {
return _attachInvokePort(
mPort,
);
}
late final _attachInvokePortPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'attachInvokePort');
late final _attachInvokePort =
_attachInvokePortPtr.asFunction<void Function(int)>();
void quickStart(
ffi.Pointer<ffi.Char> initParamsChar,
ffi.Pointer<ffi.Char> dirChar,
ffi.Pointer<ffi.Char> paramsChar,
ffi.Pointer<ffi.Char> stateParamsChar,
int port,
) {
return _quickStart(
initParamsChar,
dirChar,
paramsChar,
stateParamsChar,
port,
@@ -2556,21 +2479,19 @@ class ClashFFI {
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>, int)>();
int startTUN(
ffi.Pointer<ffi.Char> startTUN(
int fd,
ffi.Pointer<ffi.Void> callback,
) {
return _startTUN(
fd,
callback,
);
}
late final _startTUNPtr = _lookup<
ffi.NativeFunction<GoUint8 Function(ffi.Int, ffi.Pointer<ffi.Void>)>>(
'startTUN');
late final _startTUNPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'startTUN');
late final _startTUN =
_startTUNPtr.asFunction<int Function(int, ffi.Pointer<ffi.Void>)>();
_startTUNPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime();
@@ -2590,6 +2511,20 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap(
ffi.Pointer<ffi.Char> fdIdChar,
) {
return _setFdMap(
fdIdChar,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setFdMap');
late final _setFdMap =
_setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName();
}
@@ -2637,6 +2572,20 @@ class ClashFFI {
'updateDns');
late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void setProcessMap(
ffi.Pointer<ffi.Char> s,
) {
return _setProcessMap(
s,
);
}
late final _setProcessMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setProcessMap');
late final _setProcessMap =
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
}
final class __mbstate_t extends ffi.Union {
@@ -3789,31 +3738,6 @@ typedef mode_t = __darwin_mode_t;
typedef __darwin_mode_t = __uint16_t;
typedef __uint16_t = ffi.UnsignedShort;
typedef Dart__uint16_t = int;
typedef protect_func = ffi.Pointer<ffi.NativeFunction<protect_funcFunction>>;
typedef protect_funcFunction = ffi.Void Function(
ffi.Pointer<ffi.Void> tun_interface, ffi.Int fd);
typedef Dartprotect_funcFunction = void Function(
ffi.Pointer<ffi.Void> tun_interface, int fd);
typedef resolve_process_func
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
typedef resolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Void> tun_interface,
ffi.Int protocol,
ffi.Pointer<ffi.Char> source,
ffi.Pointer<ffi.Char> target,
ffi.Int uid);
typedef Dartresolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Void> tun_interface,
int protocol,
ffi.Pointer<ffi.Char> source,
ffi.Pointer<ffi.Char> target,
int uid);
typedef release_object_func
= ffi.Pointer<ffi.NativeFunction<release_object_funcFunction>>;
typedef release_object_funcFunction = ffi.Void Function(
ffi.Pointer<ffi.Void> obj);
typedef Dartrelease_object_funcFunction = void Function(
ffi.Pointer<ffi.Void> obj);
final class GoInterface extends ffi.Struct {
external ffi.Pointer<ffi.Void> t;
@@ -3834,8 +3758,6 @@ final class GoSlice extends ffi.Struct {
typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int;
typedef GoUint8 = ffi.UnsignedChar;
typedef DartGoUint8 = int;
const int __has_safe_buffers = 1;
@@ -4051,8 +3973,6 @@ const int __MAC_15_0 = 150000;
const int __MAC_15_1 = 150100;
const int __MAC_15_2 = 150200;
const int __IPHONE_2_0 = 20000;
const int __IPHONE_2_1 = 20100;
@@ -4215,8 +4135,6 @@ const int __IPHONE_18_0 = 180000;
const int __IPHONE_18_1 = 180100;
const int __IPHONE_18_2 = 180200;
const int __WATCHOS_1_0 = 10000;
const int __WATCHOS_2_0 = 20000;
@@ -4315,8 +4233,6 @@ const int __WATCHOS_11_0 = 110000;
const int __WATCHOS_11_1 = 110100;
const int __WATCHOS_11_2 = 110200;
const int __TVOS_9_0 = 90000;
const int __TVOS_9_1 = 90100;
@@ -4417,8 +4333,6 @@ const int __TVOS_18_0 = 180000;
const int __TVOS_18_1 = 180100;
const int __TVOS_18_2 = 180200;
const int __BRIDGEOS_2_0 = 20000;
const int __BRIDGEOS_3_0 = 30000;
@@ -4475,8 +4389,6 @@ const int __BRIDGEOS_9_0 = 90000;
const int __BRIDGEOS_9_1 = 90100;
const int __BRIDGEOS_9_2 = 90200;
const int __DRIVERKIT_19_0 = 190000;
const int __DRIVERKIT_20_0 = 200000;
@@ -4507,8 +4419,6 @@ const int __DRIVERKIT_24_0 = 240000;
const int __DRIVERKIT_24_1 = 240100;
const int __DRIVERKIT_24_2 = 240200;
const int __VISIONOS_1_0 = 10000;
const int __VISIONOS_1_1 = 10100;
@@ -4519,8 +4429,6 @@ const int __VISIONOS_2_0 = 20000;
const int __VISIONOS_2_1 = 20100;
const int __VISIONOS_2_2 = 20200;
const int MAC_OS_X_VERSION_10_0 = 1000;
const int MAC_OS_X_VERSION_10_1 = 1010;
@@ -4647,11 +4555,9 @@ const int MAC_OS_VERSION_15_0 = 150000;
const int MAC_OS_VERSION_15_1 = 150100;
const int MAC_OS_VERSION_15_2 = 150200;
const int __MAC_OS_X_VERSION_MIN_REQUIRED = 150000;
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150200;
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150100;
const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1;

View File

@@ -2,12 +2,15 @@ import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/common/future.dart';
import 'package:fl_clash/common/other.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart' hide Action;
mixin ClashInterface {
Future<bool> init(InitParams params);
Future<bool> init(String homeDir);
Future<bool> preload();
@@ -63,10 +66,6 @@ mixin ClashInterface {
FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections();
FutureOr<String> getProfile(String id);
Future<bool> setState(CoreState state);
}
mixin AndroidClashInterface {
@@ -74,10 +73,14 @@ mixin AndroidClashInterface {
Future<bool> setProcessMap(ProcessMapItem item);
// Future<bool> stopTun();
Future<bool> setState(CoreState state);
Future<bool> stopTun();
Future<bool> updateDns(String value);
Future<DateTime?> startTun(int fd);
Future<AndroidVpnOptions?> getAndroidVpnOptions();
Future<String> getCurrentProfileName();
@@ -103,7 +106,6 @@ abstract class ClashHandlerInterface with ClashInterface {
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
case ActionMethod.setState:
completer?.complete(result.data as bool);
return;
case ActionMethod.changeProxy:
@@ -135,7 +137,7 @@ abstract class ClashHandlerInterface with ClashInterface {
completer?.complete(result.data);
}
} catch (_) {
commonPrint.log(result.id);
debugPrint(result.id);
}
}
@@ -189,18 +191,10 @@ abstract class ClashHandlerInterface with ClashInterface {
}
@override
Future<bool> init(InitParams params) {
Future<bool> init(String homeDir) {
return invoke<bool>(
method: ActionMethod.initClash,
data: json.encode(params),
);
}
@override
Future<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
data: homeDir,
);
}
@@ -238,7 +232,6 @@ abstract class ClashHandlerInterface with ClashInterface {
return await invoke<String>(
method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams),
timeout: Duration(minutes: 2),
);
}
@@ -246,7 +239,6 @@ abstract class ClashHandlerInterface with ClashInterface {
Future<String> getProxies() {
return invoke<String>(
method: ActionMethod.getProxies,
timeout: Duration(seconds: 5),
);
}
@@ -326,14 +318,6 @@ abstract class ClashHandlerInterface with ClashInterface {
);
}
@override
Future<String> getProfile(String id) {
return invoke<String>(
method: ActionMethod.getProfile,
data: id,
);
}
@override
FutureOr<String> getTotalTraffic() {
return invoke<String>(

View File

@@ -67,6 +67,7 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
switch (result.method) {
case ActionMethod.setFdMap:
case ActionMethod.setProcessMap:
case ActionMethod.setState:
case ActionMethod.stopTun:
case ActionMethod.updateDns:
completer?.complete(result.data as bool);
@@ -122,12 +123,33 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
);
}
// @override
// Future<bool> stopTun() {
// return invoke<bool>(
// method: ActionMethod.stopTun,
// );
// }
@override
Future<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@override
Future<DateTime?> startTun(int fd) async {
final res = await invoke<String>(
method: ActionMethod.startTun,
data: json.encode(fd),
);
if (res.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
}
@override
Future<bool> stopTun() {
return invoke<bool>(
method: ActionMethod.stopTun,
);
}
@override
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
@@ -211,12 +233,37 @@ class ClashLibHandler {
);
}
attachInvokePort(int invokePort) {
clashFFI.attachInvokePort(
invokePort,
);
}
DateTime? startTun(int fd) {
final runTimeRaw = clashFFI.startTUN(fd);
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
stopTun() {
clashFFI.stopTun();
}
updateDns(String dns) {
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
}
setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar);
@@ -267,8 +314,14 @@ class ClashLibHandler {
return true;
}
setFdMap(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.setFdMap(idChar);
malloc.free(idChar);
}
Future<String> quickStart(
InitParams initParams,
String homeDir,
UpdateConfigParams updateConfigParams,
CoreState state,
) {
@@ -281,18 +334,17 @@ class ClashLibHandler {
}
});
final params = json.encode(updateConfigParams);
final initValue = json.encode(initParams);
final stateParams = json.encode(state);
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
final homeChar = homeDir.toNativeUtf8().cast<Char>();
final paramsChar = params.toNativeUtf8().cast<Char>();
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
clashFFI.quickStart(
initParamsChar,
homeChar,
paramsChar,
stateParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(initParamsChar);
malloc.free(homeChar);
malloc.free(paramsChar);
malloc.free(stateParamsChar);
return completer.future;

View File

@@ -6,7 +6,6 @@ import 'dart:typed_data';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/state.dart';
class ClashService extends ClashHandlerInterface {
static ClashService? _instance;
@@ -15,8 +14,6 @@ class ClashService extends ClashHandlerInterface {
Completer<Socket> socketCompleter = Completer();
bool isStarting = false;
Process? process;
factory ClashService() {
@@ -30,61 +27,48 @@ class ClashService extends ClashHandlerInterface {
}
_initServer() async {
runZonedGuarded(() async {
final address = !Platform.isWindows
? InternetAddress(
unixSocketPath,
type: InternetAddressType.unix,
)
: InternetAddress(
localhost,
type: InternetAddressType.IPv4,
);
await _deleteSocketFile();
final server = await ServerSocket.bind(
address,
0,
shared: true,
);
serverCompleter.complete(server);
await for (final socket in server) {
await _destroySocket();
socketCompleter.complete(socket);
socket
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
final address = !Platform.isWindows
? InternetAddress(
unixSocketPath,
type: InternetAddressType.unix,
)
: InternetAddress(
localhost,
type: InternetAddressType.IPv4,
);
await _deleteSocketFile();
final server = await ServerSocket.bind(
address,
0,
shared: true,
);
serverCompleter.complete(server);
await for (final socket in server) {
await _destroySocket();
socketCompleter.complete(socket);
socket
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
);
}
}, (error, stack) {
commonPrint.log(error.toString());
if(error is SocketException){
globalState.showNotifier(error.toString());
globalState.appController.restartCore();
}
});
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
},
);
}
}
@override
reStart() async {
if (isStarting == true) {
return;
}
isStarting = true;
socketCompleter = Completer();
if (process != null) {
await shutdown();
}
@@ -106,7 +90,6 @@ class ClashService extends ClashHandlerInterface {
],
);
process!.stdout.listen((_) {});
isStarting = false;
}
@override
@@ -142,6 +125,7 @@ class ClashService extends ClashHandlerInterface {
@override
shutdown() async {
await super.shutdown();
if (Platform.isWindows) {
await request.stopCoreByHelper();
}

View File

@@ -1,40 +1,21 @@
import 'package:flutter/material.dart';
extension ColorExtension on Color {
Color get opacity80 {
return withAlpha(204);
Color get toLight {
return withOpacity(0.8);
}
Color get opacity60 {
return withAlpha(153);
Color get toLighter {
return withOpacity(0.6);
}
Color get opacity50 {
return withAlpha(128);
Color get toSoft {
return withOpacity(0.12);
}
Color get opacity38 {
return withAlpha(97);
}
Color get opacity30 {
return withAlpha(77);
}
Color get opacity15 {
return withAlpha(38);
}
Color get opacity10 {
return withAlpha(15);
}
Color get opacity3 {
return withAlpha(76);
}
Color get opacity0 {
return withAlpha(0);
Color get toLittle {
return withOpacity(0.03);
}
Color darken([double amount = .1]) {
@@ -70,7 +51,7 @@ extension ColorExtension on Color {
}
extension ColorSchemeExtension on ColorScheme {
ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
? copyWith(
surface: Colors.black,
surfaceContainer: surfaceContainer.darken(

View File

@@ -35,5 +35,4 @@ export 'tray.dart';
export 'window.dart';
export 'windows.dart';
export 'render.dart';
export 'mixin.dart';
export 'print.dart';
export 'view.dart';

View File

@@ -11,8 +11,6 @@ import 'package:flutter/material.dart';
const appName = "FlClash";
const appHelperService = "FlClashHelperService";
const coreName = "clash.meta";
const browserUa =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
@@ -21,11 +19,6 @@ const baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
);
double textScaleFactor = min(
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
1.2,
);
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
@@ -40,6 +33,16 @@ final double kHeaderHeight = system.isDesktop
? 40
: 28
: 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};
const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
const clashConfigKey = "clash_config";
@@ -50,8 +53,10 @@ const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600;
const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204";
final commonFilter = ImageFilter.blur(
final filter = ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,
tileMode: TileMode.mirror,
@@ -81,7 +86,7 @@ const viewModeColumnsMap = {
const defaultPrimaryColor = Colors.brown;
double getWidgetHeight(num lines) {
return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0);
return max(lines * 84 + (lines - 1) * 16, 0);
}
final mainIsolate = "FlClashMainIsolate";

View File

@@ -1,7 +1,7 @@
import 'dart:async';
class Debouncer {
final Map<dynamic, Timer?> _operations = {};
final Map<dynamic, Timer> _operations = {};
call(
dynamic tag,
@@ -28,15 +28,14 @@ class Debouncer {
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
}
class Throttler {
final Map<dynamic, Timer?> _operations = {};
final Map<dynamic, Timer> _operations = {};
call(
dynamic tag,
String tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
@@ -61,27 +60,10 @@ class Throttler {
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
}
Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = Duration.zero,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
final res = await task();
if (!retryIf(res) || attempts >= maxAttempts) {
return res;
}
attempts++;
}
throw "unknown error";
}
final debouncer = Debouncer();
final throttler = Throttler();
final throttler = Throttler();

View File

@@ -10,7 +10,7 @@ extension CompleterExt<T> on Completer<T> {
FutureOr<T> Function()? onTimeout,
required String functionName,
}) {
final realTimeout = timeout ?? const Duration(seconds: 30);
final realTimeout = timeout ?? const Duration(seconds: 1);
Timer(realTimeout + commonDuration, () {
if (onLast != null) {
onLast();

View File

@@ -1,24 +1,26 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides {
static String handleFindProxy(Uri url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
final port = globalState.config.patchClashConfig.mixedPort;
final isStart = globalState.appState.runTime != null;
commonPrint.log("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
}
@override
HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context);
client.findProxy = handleFindProxy;
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = (url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;
final isStart = appController.appFlowingState.isStart;
debugPrint("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
};
return client;
}
}

View File

@@ -65,12 +65,3 @@ extension DoubleListExt on List<double> {
return -1;
}
}
extension MapExt<K, V> on Map<K, V> {
getCacheValue(K key, V defaultValue) {
if (this[key] == null) {
this[key] = defaultValue;
}
return this[key];
}
}

View File

@@ -1,8 +1,7 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'print.dart';
import 'package:flutter/material.dart';
typedef InstallConfigCallBack = void Function(String url);
@@ -16,11 +15,11 @@ class LinkManager {
}
initAppLinksListen(installConfigCallBack) async {
commonPrint.log("initAppLinksListen");
debugPrint("initAppLinksListen");
destroy();
subscription = _appLinks.uriLinkStream.listen(
(uri) {
commonPrint.log('onAppLink: $uri');
debugPrint('onAppLink: $uri');
if (uri.host == 'install-config') {
final parameters = uri.queryParameters;
final url = parameters['url'];

View File

@@ -2,9 +2,9 @@ import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list;
final List<T> _list = [];
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
FixedList(this.maxLength);
add(T item) {
if (_list.length == maxLength) {
@@ -13,26 +13,15 @@ class FixedList<T> {
_list.add(item);
}
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
class FixedMap<K, V> {
int maxSize;
final int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
@@ -45,21 +34,15 @@ class FixedMap<K, V> {
}
_map[key] = value;
_queue.add(key);
return value;
}
clear() {
clear(){
_map.clear();
_queue.clear();
}
updateMaxSize(int size){
maxSize = size;
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;

View File

@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
class Measure {
final TextScaler _textScale;
final BuildContext context;
late BuildContext context;
Measure.of(this.context)
: _textScale = TextScaler.linear(
textScaleFactor,
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
);
Size computeTextSize(
@@ -16,10 +16,7 @@ class Measure {
double maxWidth = double.infinity,
}) {
final textPainter = TextPainter(
text: TextSpan(
text: text.data,
style: text.style,
),
text: TextSpan(text: text.data, style: text.style),
maxLines: text.maxLines,
textScaler: _textScale,
textDirection: text.textDirection ?? TextDirection.ltr,

View File

@@ -1,53 +0,0 @@
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';
import 'context.dart';
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
set value(T value) {
state = value;
}
@override
bool updateShouldNotify(previous, next) {
final res = super.updateShouldNotify(previous, next);
if (res) {
onUpdate(next);
}
return res;
}
onUpdate(T value) {}
}
mixin PageMixin<T extends StatefulWidget> on State<T> {
void onPageShow() {
initPageState();
}
initPageState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
commonScaffoldState?.updateSearchState(
(_) => onSearch != null
? AppBarSearchState(
onSearch: onSearch!,
)
: null,
);
});
}
void onPageHidden() {}
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -6,82 +6,55 @@ import 'package:flutter/material.dart';
class Navigation {
static Navigation? _instance;
List<NavigationItem> getItems({
getItems({
bool openLogs = false,
bool hasProxies = false,
}) {
return [
const NavigationItem(
icon: Icon(Icons.space_dashboard),
label: PageLabel.dashboard,
keep: false,
fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard),
),
label: "dashboard",
fragment: DashboardFragment(),
),
NavigationItem(
icon: const Icon(Icons.article),
label: PageLabel.proxies,
fragment: const ProxiesFragment(
key: GlobalObjectKey(
PageLabel.proxies,
),
),
icon: const Icon(Icons.rocket),
label: "proxies",
fragment: const ProxiesFragment(),
modes: hasProxies
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
: [],
),
const NavigationItem(
icon: Icon(Icons.folder),
label: PageLabel.profiles,
fragment: ProfilesFragment(
key: GlobalObjectKey(
PageLabel.profiles,
),
),
label: "profiles",
fragment: ProfilesFragment(),
),
const NavigationItem(
icon: Icon(Icons.view_timeline),
label: PageLabel.requests,
fragment: RequestsFragment(
key: GlobalObjectKey(
PageLabel.requests,
),
),
label: "requests",
fragment: RequestsFragment(),
description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
const NavigationItem(
icon: Icon(Icons.ballot),
label: PageLabel.connections,
fragment: ConnectionsFragment(
key: GlobalObjectKey(
PageLabel.connections,
),
),
label: "connections",
fragment: ConnectionsFragment(),
description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
const NavigationItem(
icon: Icon(Icons.storage),
label: PageLabel.resources,
label: "resources",
description: "resourcesDesc",
keep: false,
fragment: Resources(
key: GlobalObjectKey(
PageLabel.resources,
),
),
fragment: Resources(),
modes: [NavigationItemMode.more],
),
NavigationItem(
icon: const Icon(Icons.adb),
label: PageLabel.logs,
fragment: const LogsFragment(
key: GlobalObjectKey(
PageLabel.logs,
),
),
label: "logs",
fragment: const LogsFragment(),
description: "logsDesc",
modes: openLogs
? [NavigationItemMode.desktop, NavigationItemMode.more]
@@ -89,12 +62,8 @@ class Navigation {
),
const NavigationItem(
icon: Icon(Icons.construction),
label: PageLabel.tools,
fragment: ToolsFragment(
key: GlobalObjectKey(
PageLabel.tools,
),
),
label: "tools",
fragment: ToolsFragment(),
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
),
];

View File

@@ -1,12 +1,10 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async {
if (globalState.appState.viewMode != ViewMode.mobile) {
if (!globalState.appController.isMobileView) {
return await Navigator.of(context).push<T>(
CommonDesktopRoute(
builder: (context) => child,
@@ -70,7 +68,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
Duration get transitionDuration => const Duration(milliseconds: 500);
@override
Duration get reverseTransitionDuration => const Duration(milliseconds: 500);
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
}
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
@@ -194,7 +192,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
_primaryPositionCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation,
curve: Curves.fastEaseInToSlowEaseOut,
reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped,
reverseCurve: Curves.easeInOut,
);
_secondaryPositionCurve = CurvedAnimation(
parent: widget.secondaryRouteAnimation,
@@ -218,8 +216,9 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
begin: const _CommonEdgeShadowDecoration(),
end: _CommonEdgeShadowDecoration(
<Color>[
widget.context.colorScheme.inverseSurface
.withValues(alpha: 0.02),
widget.context.colorScheme.inverseSurface.withOpacity(
0.06,
),
Colors.transparent,
],
),
@@ -273,7 +272,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
return;
}
final double shadowWidth = 1 * configuration.size!.width;
final double shadowWidth = 0.05 * configuration.size!.width;
final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1);

View File

@@ -2,15 +2,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
extension NumExt on num {
String fixed({decimals = 2}) {
String formatted = toStringAsFixed(decimals);
if (formatted.contains('.')) {
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
if (formatted.endsWith('.')) {
formatted = formatted.substring(0, formatted.length - 1);
}
}
return formatted;
String fixed({digit = 2}) {
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
}
}

View File

@@ -241,6 +241,11 @@ class Other {
return "${appName}_${DateTime.now().show}.log";
}
Size getScreenSize() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false,

View File

@@ -4,19 +4,18 @@ import 'dart:convert';
import 'package:fl_clash/models/models.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'constant.dart';
class Preferences {
static Preferences? _instance;
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
Future<bool> get isInit async =>
await sharedPreferencesCompleter.future != null;
Future<bool> get isInit async => await sharedPreferencesCompleter.future != null;
Preferences._internal() {
SharedPreferences.getInstance()
.then((value) => sharedPreferencesCompleter.complete(value))
.onError((_, __) => sharedPreferencesCompleter.complete(null));
SharedPreferences.getInstance().then((value) => sharedPreferencesCompleter.complete(value))
.onError((_,__)=>sharedPreferencesCompleter.complete(null));
}
factory Preferences() {
@@ -24,6 +23,7 @@ class Preferences {
return _instance!;
}
Future<ClashConfig?> getClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences?.getString(clashConfigKey);
@@ -32,26 +32,29 @@ class Preferences {
return ClashConfig.fromJson(clashConfigMap);
}
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.setString(
clashConfigKey,
json.encode(clashConfig),
);
return true;
}
Future<Config?> getConfig() async {
final preferences = await sharedPreferencesCompleter.future;
final configString = preferences?.getString(configKey);
if (configString == null) return null;
final configMap = json.decode(configString);
return Config.compatibleFromJson(configMap);
return Config.fromJson(configMap);
}
Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future;
return await preferences?.setString(
configKey,
json.encode(config),
) ??
false;
}
clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
configKey,
json.encode(config),
) ?? false;
}
clearPreferences() async {

View File

@@ -1,31 +0,0 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
class CommonPrint {
static CommonPrint? _instance;
CommonPrint._internal();
factory CommonPrint() {
_instance ??= CommonPrint._internal();
return _instance!;
}
log(String? text) {
final payload = "[FlClash] $text";
debugPrint(payload);
if (globalState.isService) {
return;
}
globalState.appController.addLog(
Log(
logLevel: LogLevel.info,
payload: payload,
),
);
}
}
final commonPrint = CommonPrint();

View File

@@ -14,13 +14,15 @@ class Protocol {
void register(String scheme) {
String protocolRegKey = 'Software\\Classes\\$scheme';
RegistryValue protocolRegValue = RegistryValue.string(
RegistryValue protocolRegValue = const RegistryValue(
'URL Protocol',
RegistryValueType.string,
'',
);
String protocolCmdRegKey = 'shell\\open\\command';
RegistryValue protocolCmdRegValue = RegistryValue.string(
RegistryValue protocolCmdRegValue = RegistryValue(
'',
RegistryValueType.string,
'"${Platform.resolvedExecutable}" "%1"',
);
final regKey = Registry.currentUser.createKey(protocolRegKey);
@@ -29,4 +31,4 @@ class Protocol {
}
}
final protocol = Protocol();
final protocol = Protocol();

View File

@@ -1,5 +1,5 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart';
class Render {
@@ -22,26 +22,26 @@ class Render {
}
pause() {
throttler.call(
DebounceTag.renderPause,
debouncer.call(
"render_pause",
_pause,
duration: Duration(seconds: 5),
);
}
resume() {
throttler.cancel(DebounceTag.renderPause);
debouncer.cancel("render_pause");
_resume();
}
void _pause() async {
void _pause() {
if (_isPaused) return;
_isPaused = true;
_beginFrame = _dispatcher.onBeginFrame;
_drawFrame = _dispatcher.onDrawFrame;
_dispatcher.onBeginFrame = null;
_dispatcher.onDrawFrame = null;
commonPrint.log("pause");
debugPrint("[App] pause");
}
void _resume() {
@@ -50,7 +50,7 @@ class Render {
_dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame;
_dispatcher.scheduleFrame();
commonPrint.log("resume");
debugPrint("[App] resume");
}
}

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
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/state.dart';
@@ -11,35 +11,33 @@ import 'package:flutter/cupertino.dart';
class Request {
late final Dio _dio;
late final Dio _clashDio;
String? userAgent;
Request() {
_dio = Dio(
BaseOptions(
headers: {
"User-Agent": browserUa,
_dio = Dio();
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
return handler.next(options); // 继续请求
},
),
);
_clashDio = Dio();
_clashDio.httpClientAdapter = IOHttpClientAdapter(createHttpClient: () {
final client = HttpClient();
client.findProxy = (Uri uri) {
client.userAgent = globalState.ua;
return FlClashHttpOverrides.handleFindProxy(uri);
};
return client;
});
}
Future<Response> getFileResponseForUrl(String url) async {
final response = await _clashDio.get(
url,
options: Options(
responseType: ResponseType.bytes,
),
);
final response = await _dio
.get(
url,
options: Options(
headers: {
"User-Agent": globalState.appController.clashConfig.globalUa
},
responseType: ResponseType.bytes,
),
)
.timeout(
httpTimeoutDuration * 6,
);
return response;
}
@@ -73,38 +71,31 @@ class Request {
return data;
}
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
final List<String> _ipInfoSources = [
"https://ipwho.is/?fields=ip&output=csv",
"https://ipinfo.io/ip",
"https://ifconfig.me/ip/",
];
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) {
for (final source in _ipInfoSources) {
try {
final response = await Dio()
.get<Map<String, dynamic>>(
source.key,
final response = await _dio
.get<String>(
source,
cancelToken: cancelToken,
options: Options(
responseType: ResponseType.json,
),
)
.timeout(
Duration(
seconds: 30,
),
);
.timeout(httpTimeoutDuration);
if (response.statusCode != 200 || response.data == null) {
continue;
}
if (response.data == null) {
final ipInfo = await clashCore.getCountryCode(response.data!);
if (ipInfo == null && source != _ipInfoSources.last) {
continue;
}
return source.value(response.data!);
return ipInfo;
} catch (e) {
commonPrint.log("checkIp error ===> $e");
debugPrint("checkIp error ===> $e");
if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled";
}

View File

@@ -2,7 +2,6 @@ import 'dart:math';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/widgets/scroll.dart';
import 'package:flutter/material.dart';
class BaseScrollBehavior extends MaterialScrollBehavior {
@@ -17,6 +16,8 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
};
}
class BaseScrollBehavior2 extends ScrollBehavior {}
class HiddenBarScrollBehavior extends BaseScrollBehavior {
@override
Widget buildScrollbar(
@@ -35,7 +36,8 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
Widget child,
ScrollableDetails details,
) {
return CommonAutoHiddenScrollBar(
return Scrollbar(
interactive: true,
controller: details.controller,
child: child,
);

View File

View File

@@ -1,7 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'print.dart';
import 'package:flutter/material.dart';
extension StringExtension on String {
bool get isUrl {
@@ -43,17 +43,8 @@ extension StringExtension on String {
RegExp(this);
return true;
} catch (e) {
commonPrint.log(e.toString());
debugPrint(e.toString());
return false;
}
}
}
extension StringExtensionSafe on String? {
String getSafeValue(String defaultValue) {
if (this == null || this!.isEmpty) {
return defaultValue;
}
return this!;
}
}

View File

@@ -46,7 +46,7 @@ class System {
} else if (Platform.isLinux) {
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
final output = result.stdout.trim();
if (output.startsWith('root:') && output.contains('rws')) {
if (output.startsWith('root:') && output.contains('rwx')) {
return true;
}
return false;

View File

@@ -1,20 +1,15 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'color.dart';
extension TextStyleExtension on TextStyle {
TextStyle get toLight => copyWith(color: color?.opacity80);
TextStyle get toLight => copyWith(color: color?.toLight);
TextStyle get toLighter => copyWith(color: color?.opacity60);
TextStyle get toLighter => copyWith(color: color?.toLighter);
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
TextStyle get toJetBrainsMono => copyWith(
fontFamily: FontFamily.jetBrainsMono.value,
);
TextStyle adjustSize(int size) => copyWith(
fontSize: fontSize! + size,
);

View File

@@ -1,39 +0,0 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
class CommonTheme {
final BuildContext context;
final Map<String, Color> _colorMap;
CommonTheme.of(this.context) : _colorMap = {};
Color get darkenSecondaryContainer {
return _colorMap.getCacheValue(
"darkenSecondaryContainer",
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.1),
);
}
Color get darkenSecondaryContainerLighter {
return _colorMap.getCacheValue(
"darkenSecondaryContainerLighter",
context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.opacity60,
);
}
Color get darken2SecondaryContainer {
return _colorMap.getCacheValue(
"darken2SecondaryContainer",
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.2),
);
}
Color get darken3PrimaryContainer {
return _colorMap.getCacheValue(
"darken3PrimaryContainer",
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3),
);
}
}

View File

@@ -39,7 +39,10 @@ class Tray {
}
update({
required TrayState trayState,
required AppState appState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
bool focus = false,
}) async {
if (Platform.isAndroid) {
@@ -47,7 +50,7 @@ class Tray {
}
if (!Platform.isLinux) {
await _updateSystemTray(
brightness: trayState.brightness,
brightness: appState.brightness,
force: focus,
);
}
@@ -60,7 +63,9 @@ class Tray {
);
menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox(
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
label: appFlowingState.isStart
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async {
globalState.appController.updateStart();
},
@@ -75,22 +80,23 @@ class Tray {
onClick: (_) {
globalState.appController.changeMode(mode);
},
checked: mode == trayState.mode,
checked: mode == clashConfig.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (!Platform.isWindows) {
for (final group in trayState.groups) {
final groups = appState.currentGroups;
for (final group in groups) {
List<MenuItem> subMenuItems = [];
for (final proxy in group.all) {
subMenuItems.add(
MenuItem.checkbox(
label: proxy.name,
checked: trayState.selectedMap[group.name] == proxy.name,
checked: appState.selectedMap[group.name] == proxy.name,
onClick: (_) {
final appController = globalState.appController;
appController.updateCurrentSelectedMap(
appController.config.updateCurrentSelectedMap(
group.name,
proxy.name,
);
@@ -111,18 +117,18 @@ class Tray {
),
);
}
if (trayState.groups.isNotEmpty) {
if (groups.isNotEmpty) {
menuItems.add(MenuItem.separator());
}
}
if (trayState.isStart) {
if (appFlowingState.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
globalState.appController.updateTun();
},
checked: trayState.tunEnable,
checked: clashConfig.tun.enable,
),
);
menuItems.add(
@@ -131,7 +137,7 @@ class Tray {
onClick: (_) {
globalState.appController.updateSystemProxy();
},
checked: trayState.systemProxy,
checked: config.networkProps.systemProxy,
),
);
menuItems.add(MenuItem.separator());
@@ -141,12 +147,12 @@ class Tray {
onClick: (_) async {
globalState.appController.updateAutoLaunch();
},
checked: trayState.autoLaunch,
checked: config.appSetting.autoLaunch,
);
final copyEnvVarMenuItem = MenuItem(
label: appLocalizations.copyEnvVar,
onClick: (_) async {
await _copyEnv(trayState.port);
await _copyEnv(clashConfig.mixedPort);
},
);
menuItems.add(autoStartMenuItem);
@@ -163,25 +169,12 @@ class Tray {
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
await _updateSystemTray(
brightness: trayState.brightness,
brightness: appState.brightness,
force: focus,
);
}
}
updateTrayTitle([Traffic? traffic]) async {
// if (!Platform.isMacOS) {
// return;
// }
// if (traffic == null) {
// await trayManager.setTitle("");
// } else {
// await trayManager.setTitle(
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
// );
// }
}
Future<void> _copyEnv(int port) async {
final url = "http://127.0.0.1:$port";

20
lib/common/view.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'context.dart';
mixin ViewMixin<T extends StatefulWidget> on State<T> {
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
initViewState() {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onSearch = onSearch;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
}
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -1,14 +1,13 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart';
import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart';
class Window {
init(int version) async {
final props = globalState.config.windowProps;
init(WindowProps props, int version) async {
final acquire = await singleInstanceLock.acquire();
if (!acquire) {
exit(0);
@@ -21,12 +20,10 @@ class Window {
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height),
minimumSize: const Size(380, 400),
minimumSize: const Size(380, 500),
);
if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
if (!Platform.isMacOS) {
final left = props.left ?? 0;
final top = props.top ?? 0;
final right = left + props.width;
@@ -63,16 +60,14 @@ class Window {
}
show() async {
render?.resume();
await windowManager.show();
await windowManager.focus();
await windowManager.setSkipTaskbar(false);
render?.resume();
}
Future<bool> get isVisible async {
final value = await windowManager.isVisible();
commonPrint.log("window visible check: $value");
return value;
Future<bool> isVisible() async {
return await windowManager.isVisible();
}
close() async {
@@ -80,9 +75,9 @@ class Window {
}
hide() async {
render?.pause();
await windowManager.hide();
await windowManager.setSkipTaskbar(true);
render?.pause();
}
}

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/cupertino.dart';
import 'package:path/path.dart';
class Windows {
@@ -53,7 +54,7 @@ class Windows {
calloc.free(argumentsPtr);
calloc.free(operationPtr);
commonPrint.log("windows runas: $command $arguments resultCode:$result");
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
if (result < 42) {
return false;

View File

@@ -8,32 +8,31 @@ import 'package:archive/archive.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart';
import 'fragments/profiles/override_profile.dart';
import 'models/models.dart';
class AppController {
bool lastTunEnable = false;
int? lastProfileModified;
final BuildContext context;
final WidgetRef _ref;
late AppState appState;
late AppFlowingState appFlowingState;
late Config config;
late ClashConfig clashConfig;
AppController(this.context, WidgetRef ref) : _ref = ref;
AppController(this.context) {
appState = context.read<AppState>();
config = context.read<Config>();
clashConfig = context.read<ClashConfig>();
appFlowingState = context.read<AppFlowingState>();
}
updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, () async {
final isPatch = globalState.appState.needApply ? false : true;
await updateClashConfig(isPatch);
});
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
}
updateGroupsDebounce() {
@@ -42,16 +41,14 @@ class AppController {
addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
_ref.read(checkIpNumProvider.notifier).add();
appState.checkIpNum++;
});
}
applyProfileDebounce({
bool silence = false,
}) {
debouncer.call(DebounceTag.applyProfile, (silence) {
applyProfile(silence: silence);
}, args: [silence]);
applyProfileDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
applyProfile(isPrue: true);
});
}
savePreferencesDebounce() {
@@ -70,12 +67,11 @@ class AppController {
}
restartCore() async {
await clashService?.reStart();
await _initCore();
if (_ref.read(runTimeProvider.notifier).isStart) {
await globalState.handleStart();
}
await globalState.restartCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
}
updateStatus(bool isStart) async {
@@ -85,12 +81,13 @@ class AppController {
updateTraffic,
]);
final currentLastModified =
await _ref.read(currentProfileProvider)?.profileLastModified;
if (currentLastModified == null || lastProfileModified == null) {
await config.getCurrentProfile()?.profileLastModified;
if (currentLastModified == null ||
globalState.lastProfileModified == null) {
addCheckIpNumDebounce();
return;
}
if (currentLastModified <= (lastProfileModified ?? 0)) {
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
addCheckIpNumDebounce();
return;
}
@@ -98,9 +95,9 @@ class AppController {
} else {
await globalState.handleStop();
await clashCore.resetTraffic();
_ref.read(trafficsProvider.notifier).clear();
_ref.read(totalTrafficProvider.notifier).value = Traffic();
_ref.read(runTimeProvider.notifier).value = null;
appFlowingState.traffics = [];
appFlowingState.totalTraffic = Traffic();
appFlowingState.runTime = null;
addCheckIpNumDebounce();
}
}
@@ -110,218 +107,107 @@ class AppController {
if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
_ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
} else {
_ref.read(runTimeProvider.notifier).value = null;
appFlowingState.runTime = null;
}
}
updateTraffic() async {
final traffic = await clashCore.getTraffic();
_ref.read(trafficsProvider.notifier).addTraffic(traffic);
_ref.read(totalTrafficProvider.notifier).value =
await clashCore.getTotalTraffic();
updateTraffic() {
globalState.updateTraffic(
config: config,
appFlowingState: appFlowingState,
);
}
addProfile(Profile profile) async {
_ref.read(profilesProvider.notifier).setProfile(profile);
if (_ref.read(currentProfileIdProvider) != null) return;
_ref.read(currentProfileIdProvider.notifier).value = profile.id;
config.setProfile(profile);
if (config.currentProfileId != null) return;
await changeProfile(profile.id);
}
deleteProfile(String id) async {
_ref.read(profilesProvider.notifier).deleteProfileById(id);
config.deleteProfileById(id);
clearEffect(id);
if (globalState.config.currentProfileId == id) {
final profiles = globalState.config.profiles;
final currentProfileId = _ref.read(currentProfileIdProvider.notifier);
if (profiles.isNotEmpty) {
final updateId = profiles.first.id;
currentProfileId.value = updateId;
if (config.currentProfileId == id) {
if (config.profiles.isNotEmpty) {
final updateId = config.profiles.first.id;
changeProfile(updateId);
} else {
currentProfileId.value = null;
changeProfile(null);
updateStatus(false);
}
}
}
updateProviders() async {
_ref.read(providersProvider.notifier).value =
await clashCore.getExternalProviders();
await globalState.updateProviders(appState);
}
updateLocalIp() async {
_ref.read(localIpProvider.notifier).value = null;
appFlowingState.localIp = null;
await Future.delayed(commonDuration);
_ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress();
appFlowingState.localIp = await other.getLocalIpAddress();
}
Future<void> updateProfile(Profile profile) async {
final newProfile = await profile.update();
_ref
.read(profilesProvider.notifier)
.setProfile(newProfile.copyWith(isUpdating: false));
if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(silence: true);
config.setProfile(
newProfile.copyWith(isUpdating: false),
);
if (profile.id == config.currentProfile?.id) {
applyProfileDebounce();
}
}
setProfile(Profile profile) {
_ref.read(profilesProvider.notifier).setProfile(profile);
}
setProfileAndAutoApply(Profile profile) {
_ref.read(profilesProvider.notifier).setProfile(profile);
if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(silence: true);
config.setProfile(profile);
if (profile.id == config.currentProfile?.id) {
applyProfileDebounce();
}
}
setProfiles(List<Profile> profiles) {
_ref.read(profilesProvider.notifier).value = profiles;
}
addLog(Log log) {
_ref.read(logsProvider).add(log);
}
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
final hotKeyActions = _ref.read(hotKeyActionsProvider);
final index =
hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
if (index == -1) {
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
..add(hotKeyAction);
} else {
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
..[index] = hotKeyAction;
}
_ref.read(hotKeyActionsProvider.notifier).value = index == -1
? (List.from(hotKeyActions)..add(hotKeyAction))
: (List.from(hotKeyActions)..[index] = hotKeyAction);
}
List<Group> getCurrentGroups() {
return _ref.read(currentGroupsStateProvider.select((state) => state.value));
}
String getRealTestUrl(String? url) {
return _ref.read(getRealTestUrlProvider(url));
}
int getProxiesColumns() {
return _ref.read(getProxiesColumnsProvider);
}
addSortNum() {
return _ref.read(sortNumProvider.notifier).add();
}
getCurrentGroupName() {
final currentGroupName = _ref.read(currentProfileProvider.select(
(state) => state?.currentGroupName,
));
return currentGroupName;
}
ProxyCardState getProxyCardState(proxyName) {
return _ref.read(getProxyCardStateProvider(proxyName));
}
getSelectedProxyName(groupName) {
return _ref.read(getSelectedProxyNameProvider(groupName));
}
updateCurrentGroupName(String groupName) {
final profile = _ref.read(currentProfileProvider);
if (profile == null || profile.currentGroupName == groupName) {
return;
}
setProfile(
profile.copyWith(currentGroupName: groupName),
);
}
Future<void> updateClashConfig([bool? isPatch]) async {
commonPrint.log("update clash patch: ${isPatch ?? false}");
Future<void> updateClashConfig({bool isPatch = true}) async {
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await _updateClashConfig(
isPatch,
await globalState.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
);
});
}
Future<void> _updateClashConfig([bool? isPatch]) async {
final profile = _ref.watch(currentProfileProvider);
await _ref.read(currentProfileProvider)?.checkAndUpdate();
final patchConfig = _ref.read(patchClashConfigProvider);
final appSetting = _ref.read(appSettingProvider);
bool enableTun = patchConfig.tun.enable;
if (enableTun != lastTunEnable &&
lastTunEnable == false &&
!Platform.isAndroid) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.none:
break;
case AuthorizeCode.success:
lastTunEnable = enableTun;
await restartCore();
return;
case AuthorizeCode.error:
enableTun = false;
}
}
if (appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
final res = await clashCore.updateConfig(
globalState.getUpdateConfigParams(isPatch),
);
if (isPatch == false) {
_ref.read(needApplyProvider.notifier).value = false;
}
if (res.isNotEmpty) throw res;
lastTunEnable = enableTun;
lastProfileModified = await profile?.profileLastModified;
}
Future _applyProfile() async {
await clashCore.requestGc();
await updateClashConfig();
await updateGroups();
await updateProviders();
}
Future applyProfile({bool silence = false}) async {
if (silence) {
await _applyProfile();
Future applyProfile({bool isPrue = false}) async {
if (isPrue) {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
} else {
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await _applyProfile();
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
});
}
addCheckIpNumDebounce();
}
handleChangeProfile() {
_ref.read(delayDataSourceProvider.notifier).value = {};
applyProfile();
}
updateBrightness(Brightness brightness) {
_ref.read(appBrightnessProvider.notifier).value = brightness;
changeProfile(String? value) async {
if (value == config.currentProfileId) return;
config.currentProfileId = value;
}
autoUpdateProfiles() async {
for (final profile in _ref.read(profilesProvider)) {
for (final profile in config.profiles) {
if (!profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate
?.add(
@@ -332,29 +218,20 @@ class AppController {
continue;
}
try {
await updateProfile(profile);
updateProfile(profile);
} catch (e) {
_ref.read(logsProvider.notifier).addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
appFlowingState.addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
}
}
}
Future<void> updateGroups() async {
_ref.read(groupsProvider.notifier).value = await retry(
task: () async {
return await clashCore.getProxiesGroups();
},
retryIf: (res) => res.isEmpty,
);
}
updateProfiles() async {
for (final profile in _ref.read(profilesProvider)) {
for (final profile in config.profiles) {
if (profile.type == ProfileType.file) {
continue;
}
@@ -362,33 +239,34 @@ class AppController {
}
}
updateSystemColorSchemes(ColorSchemes colorSchemes) {
_ref.read(appSchemesProvider.notifier).value = colorSchemes;
Future<void> updateGroups() async {
await globalState.updateGroups(appState);
}
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
appState.systemColorSchemes = systemColorSchemes;
}
savePreferences() async {
commonPrint.log("save preferences");
await preferences.saveConfig(globalState.config);
debugPrint("[APP] savePreferences");
await preferences.saveConfig(config);
await preferences.saveClashConfig(clashConfig);
}
changeProxy({
required String groupName,
required String proxyName,
}) async {
await clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
await globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
);
if (_ref.read(appSettingProvider).closeConnections) {
clashCore.closeConnections();
}
addCheckIpNumDebounce();
}
handleBackOrExit() async {
if (_ref.read(appSettingProvider).minimizeOnExit) {
if (config.appSetting.minimizeOnExit) {
if (system.isDesktop) {
await savePreferencesDebounce();
}
@@ -411,7 +289,7 @@ class AppController {
}
autoCheckUpdate() async {
if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
if (!config.appSetting.autoCheckUpdate) return;
final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res);
}
@@ -420,9 +298,6 @@ class AppController {
Map<String, dynamic>? data,
bool handleError = false,
}) async {
if (globalState.isPre) {
return;
}
if (data != null) {
final tagName = data['tag_name'];
final body = data['body'];
@@ -471,7 +346,7 @@ class AppController {
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.cacheCorrupt),
);
if (res == true) {
if (res) {
final file = File(await appPath.sharedPreferencesPath);
final isExists = await file.exists();
if (isExists) {
@@ -481,43 +356,33 @@ class AppController {
await handleExit();
}
Future<void> _initCore() async {
final isInit = await clashCore.isInit;
if (!isInit) {
await clashCore.setState(
globalState.getCoreState(),
);
await clashCore.init();
}
await applyProfile();
}
init() async {
await _handlePreference();
await _handlerDisclaimer();
await _initCore();
await globalState.initCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
await _initStatus();
updateTray(true);
autoLaunch?.updateStatus(
_ref.read(appSettingProvider).autoLaunch,
config.appSetting.autoLaunch,
);
autoUpdateProfiles();
autoCheckUpdate();
if (!_ref.read(appSettingProvider).silentLaunch) {
if (!config.appSetting.silentLaunch) {
window?.show();
} else {
window?.hide();
}
_ref.read(initProvider.notifier).value = true;
}
_initStatus() async {
if (Platform.isAndroid) {
await globalState.updateStartTime();
}
final status = globalState.isStart == true
? true
: _ref.read(appSettingProvider).autoRun;
final status =
globalState.isStart == true ? true : config.appSetting.autoRun;
await updateStatus(status);
if (!status) {
@@ -526,15 +391,35 @@ class AppController {
}
setDelay(Delay delay) {
_ref.read(delayDataSourceProvider.notifier).setDelay(delay);
appState.setDelay(delay);
}
toPage(PageLabel pageLabel) {
_ref.read(currentPageLabelProvider.notifier).value = pageLabel;
toPage(
int index, {
bool hasAnimate = false,
}) {
if (index > appState.currentNavigationItems.length - 1) {
return;
}
appState.currentLabel = appState.currentNavigationItems[index].label;
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
globalState.pageController?.animateToPage(
index,
duration: kTabScrollDuration,
curve: Curves.easeOut,
);
} else {
globalState.pageController?.jumpToPage(index);
}
}
toProfiles() {
toPage(PageLabel.profiles);
final index = appState.currentNavigationItems.indexWhere(
(element) => element.label == "profiles",
);
if (index != -1) {
toPage(index);
}
}
initLink() {
@@ -571,8 +456,17 @@ class AppController {
Future<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>(
dismissible: false,
child: CommonDialog(
title: appLocalizations.disclaimer,
child: AlertDialog(
title: Text(appLocalizations.disclaimer),
content: Container(
width: dialogCommonWidth,
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: SelectableText(
appLocalizations.disclaimerDesc,
),
),
),
actions: [
TextButton(
onPressed: () {
@@ -582,24 +476,21 @@ class AppController {
),
TextButton(
onPressed: () {
_ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(disclaimerAccepted: true),
);
config.appSetting = config.appSetting.copyWith(
disclaimerAccepted: true,
);
Navigator.of(context).pop<bool>(true);
},
child: Text(appLocalizations.agree),
)
],
child: SelectableText(
appLocalizations.disclaimerDesc,
),
),
) ??
false;
}
_handlerDisclaimer() async {
if (_ref.read(appSettingProvider).disclaimerAccepted) {
if (config.appSetting.disclaimerAccepted) {
return;
}
final isDisclaimerAccepted = await showDisclaimer();
@@ -658,14 +549,22 @@ class AppController {
addProfileFormURL(url);
}
updateViewSize(Size size) {
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_ref.read(viewSizeProvider.notifier).value = size;
appState.viewWidth = width;
});
}
setProvider(ExternalProvider? provider) {
_ref.read(providersProvider.notifier).setProvider(provider);
int? getDelay(String proxyName, [String? url]) {
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
return currentDelayMap?[appState.getRealProxyName(proxyName)];
}
String getRealTestUrl(String? url) {
if (url == null || url.isEmpty) {
return config.appSetting.testUrl;
}
return url;
}
List<Proxy> _sortOfName(List<Proxy> proxies) {
@@ -678,17 +577,12 @@ class AppController {
);
}
List<Proxy> _sortOfDelay({
required List<Proxy> proxies,
String? testUrl,
}) {
List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) {
return List.of(proxies)
..sort(
(a, b) {
final aDelay =
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
final bDelay =
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
final aDelay = getDelay(a.name, url);
final bDelay = getDelay(b.name, url);
if (aDelay == null && bDelay == null) {
return 0;
}
@@ -704,102 +598,69 @@ class AppController {
}
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
return switch (config.proxiesStyle.sortType) {
ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(
proxies: proxies,
testUrl: url,
),
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
ProxiesSortType.name => _sortOfName(proxies),
};
}
String getCurrentSelectedName(String groupName) {
final group = appState.getGroupWithName(groupName);
return group?.getCurrentSelectedName(
config.currentSelectedMap[groupName] ?? '') ??
'';
}
clearEffect(String profileId) async {
final profilePath = await appPath.getProfilePath(profileId);
final providersPath = await appPath.getProvidersPath(profileId);
return await Isolate.run(() async {
if (profilePath != null) {
final profileFile = File(profilePath);
final isExists = await profileFile.exists();
if (isExists) {
profileFile.delete(recursive: true);
}
await File(profilePath).delete(recursive: true);
}
if (providersPath != null) {
final providersFileDir = File(providersPath);
final isExists = await providersFileDir.exists();
if (isExists) {
providersFileDir.delete(recursive: true);
}
await File(providersPath).delete(recursive: true);
}
});
}
bool get isMobileView {
return appState.viewMode == ViewMode.mobile;
}
updateTun() {
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(enable: !state.tun.enable),
);
clashConfig.tun = clashConfig.tun.copyWith(
enable: !clashConfig.tun.enable,
);
}
updateSystemProxy() {
_ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: !state.systemProxy,
),
);
config.networkProps = config.networkProps.copyWith(
systemProxy: !config.networkProps.systemProxy,
);
}
updateStart() {
updateStatus(!_ref.read(runTimeProvider.notifier).isStart);
}
updateCurrentSelectedMap(String groupName, String proxyName) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null &&
currentProfile.selectedMap[groupName] != proxyName) {
final SelectedMap selectedMap = Map.from(
currentProfile.selectedMap,
)..[groupName] = proxyName;
_ref.read(profilesProvider.notifier).setProfile(
currentProfile.copyWith(
selectedMap: selectedMap,
),
);
}
}
updateCurrentUnfoldSet(Set<String> value) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile == null) {
return;
}
_ref.read(profilesProvider.notifier).setProfile(
currentProfile.copyWith(
unfoldSet: value,
),
);
updateStatus(!appFlowingState.isStart);
}
changeMode(Mode mode) {
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(mode: mode),
);
clashConfig.mode = mode;
if (mode == Mode.global) {
updateCurrentGroupName(GroupName.GLOBAL.name);
config.updateCurrentGroupName(GroupName.GLOBAL.name);
}
addCheckIpNumDebounce();
}
updateAutoLaunch() {
_ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoLaunch: !state.autoLaunch,
),
);
config.appSetting = config.appSetting.copyWith(
autoLaunch: !config.appSetting.autoLaunch,
);
}
updateVisible() async {
final visible = await window?.isVisible;
final visible = await window?.isVisible();
if (visible != null && !visible) {
window?.show();
} else {
@@ -808,56 +669,18 @@ class AppController {
}
updateMode() {
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) {
final index = Mode.values.indexWhere((item) => item == state.mode);
if (index == -1) {
return null;
}
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
return state.copyWith(
mode: Mode.values[nextIndex],
);
},
);
}
handleAddOrUpdate(WidgetRef ref, [Rule? rule]) async {
final res = await globalState.showCommonDialog<Rule>(
child: AddRuleDialog(
rule: rule,
snippet: ref.read(
profileOverrideStateProvider.select(
(state) => state.snippet!,
),
),
),
);
if (res == null) {
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
if (index == -1) {
return;
}
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) {
final model = state.copyWith.overrideData!(
rule: state.overrideData!.rule.updateRules(
(rules) {
final index = rules.indexWhere((item) => item.id == res.id);
if (index == -1) {
return List.from([res, ...rules]);
}
return List.from(rules)..[index] = res;
},
),
);
return model;
},
);
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
clashConfig.mode = Mode.values[nextIndex];
}
Future<bool> exportLogs() async {
final logsRaw = _ref.read(logsProvider).list.map(
(item) => item.toString(),
);
final logsRaw = appFlowingState.logs.map(
(item) => item.toString(),
);
final data = await Isolate.run<List<int>>(() async {
final logsRawString = logsRaw.join("\n");
return utf8.encode(logsRawString);
@@ -872,10 +695,12 @@ class AppController {
Future<List<int>> backupData() async {
final homeDirPath = await appPath.homeDirPath;
final profilesPath = await appPath.profilesPath;
final configJson = globalState.config.toJson();
final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async {
final archive = Archive();
archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? [];
@@ -884,7 +709,11 @@ class AppController {
updateTray([bool focus = false]) async {
tray.update(
trayState: _ref.read(trayStateProvider),
appState: appState,
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
focus: focus,
);
}
@@ -903,64 +732,32 @@ class AppController {
archive.files.where((item) => !item.name.endsWith(".json"));
final configIndex =
configs.indexWhere((config) => config.name == "config.json");
if (configIndex == -1) throw "invalid backup file";
final clashConfigIndex =
configs.indexWhere((config) => config.name == "clashConfig.json");
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
final configFile = configs[configIndex];
var tempConfig = Config.compatibleFromJson(
final clashConfigFile = configs[clashConfigIndex];
final tempConfig = Config.fromJson(
json.decode(
utf8.decode(configFile.content),
),
);
final tempClashConfig = ClashConfig.fromJson(
json.decode(
utf8.decode(clashConfigFile.content),
),
);
for (final profile in profiles) {
final filePath = join(homeDirPath, profile.name);
final file = File(filePath);
await file.create(recursive: true);
await file.writeAsBytes(profile.content);
}
final clashConfigIndex =
configs.indexWhere((config) => config.name == "clashConfig.json");
if (clashConfigIndex != -1) {
final clashConfigFile = configs[clashConfigIndex];
tempConfig = tempConfig.copyWith(
patchClashConfig: ClashConfig.fromJson(
json.decode(
utf8.decode(
clashConfigFile.content,
),
),
),
);
if (recoveryOption == RecoveryOption.onlyProfiles) {
config.update(tempConfig, RecoveryOption.onlyProfiles);
} else {
config.update(tempConfig, RecoveryOption.all);
clashConfig.update(tempClashConfig);
}
_recovery(
tempConfig,
recoveryOption,
);
}
_recovery(Config config, RecoveryOption recoveryOption) {
final profiles = config.profiles;
for (final profile in profiles) {
_ref.read(profilesProvider.notifier).setProfile(profile);
}
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
if (onlyProfiles) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null) {
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
}
return;
}
_ref.read(patchClashConfigProvider.notifier).value =
config.patchClashConfig;
_ref.read(appSettingProvider.notifier).value = config.appSetting;
_ref.read(currentProfileIdProvider.notifier).value =
config.currentProfileId;
_ref.read(appDAVSettingProvider.notifier).value = config.dav;
_ref.read(themeSettingProvider.notifier).value = config.themeProps;
_ref.read(windowSettingProvider.notifier).value = config.windowProps;
_ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
_ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle;
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
}
}

View File

@@ -34,24 +34,7 @@ const desktopPlatforms = [
SupportPlatform.Windows,
];
enum GroupType {
Selector,
URLTest,
Fallback,
LoadBalance,
Relay;
static GroupType parseProfileType(String type) {
return switch (type) {
"url-test" => URLTest,
"select" => Selector,
"fallback" => Fallback,
"load-balance" => LoadBalance,
"relay" => Relay,
String() => throw UnimplementedError(),
};
}
}
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
@@ -62,7 +45,7 @@ extension GroupTypeExtension on GroupType {
)
.toList();
bool get isComputedSelected {
bool get isURLTestOrFallback {
return [GroupType.URLTest, GroupType.Fallback].contains(this);
}
@@ -155,13 +138,6 @@ enum DnsMode {
hosts
}
enum ExternalControllerStatus {
@JsonValue("")
close,
@JsonValue("127.0.0.1:9090")
open
}
enum KeyboardModifier {
alt([
PhysicalKeyboardKey.altLeft,
@@ -219,13 +195,14 @@ enum ProxiesIconStyle {
}
enum FontFamily {
system(),
miSans("MiSans"),
twEmoji("Twemoji"),
jetBrainsMono("JetBrainsMono"),
icon("Icons");
final String value;
final String? value;
const FontFamily(this.value);
const FontFamily([this.value]);
}
enum RouteMode {
@@ -261,7 +238,6 @@ enum ActionMethod {
stopListener,
getCountryCode,
getMemory,
getProfile,
///Android,
setFdMap,
@@ -294,11 +270,7 @@ enum DebounceTag {
handleWill,
updateDelay,
vpnTip,
autoLaunch,
renderPause,
updatePageIndex,
pageChange,
proxiesTabChange,
autoLaunch
}
enum DashboardWidget {
@@ -369,81 +341,3 @@ enum DashboardWidget {
return dashboardWidgets[index];
}
}
enum GeodataLoader {
standard,
memconservative,
}
enum PageLabel {
dashboard,
proxies,
profiles,
tools,
logs,
requests,
resources,
connections,
}
enum RuleAction {
DOMAIN("DOMAIN"),
DOMAIN_SUFFIX("DOMAIN-SUFFIX"),
DOMAIN_KEYWORD("DOMAIN-KEYWORD"),
DOMAIN_REGEX("DOMAIN-REGEX"),
GEOSITE("GEOSITE"),
IP_CIDR("IP-CIDR"),
IP_CIDR6("IP-CIDR6"),
IP_SUFFIX("IP-SUFFIX"),
IP_ASN("IP-ASN"),
GEOIP("GEOIP"),
SRC_GEOIP("SRC-GEOIP"),
SRC_IP_ASN("SRC-IP-ASN"),
SRC_IP_CIDR("SRC-IP-CIDR"),
SRC_IP_SUFFIX("SRC-IP-SUFFIX"),
DST_PORT("DST-PORT"),
SRC_PORT("SRC-PORT"),
IN_PORT("IN-PORT"),
IN_TYPE("IN-TYPE"),
IN_USER("IN-USER"),
IN_NAME("IN-NAME"),
PROCESS_PATH("PROCESS-PATH"),
PROCESS_PATH_REGEX("PROCESS-PATH-REGEX"),
PROCESS_NAME("PROCESS-NAME"),
PROCESS_NAME_REGEX("PROCESS-NAME-REGEX"),
UID("UID"),
NETWORK("NETWORK"),
DSCP("DSCP"),
RULE_SET("RULE-SET"),
AND("AND"),
OR("OR"),
NOT("NOT"),
SUB_RULE("SUB-RULE"),
MATCH("MATCH");
final String value;
const RuleAction(this.value);
}
extension RuleActionExt on RuleAction {
bool get hasParams => [
RuleAction.GEOIP,
RuleAction.IP_ASN,
RuleAction.SRC_IP_ASN,
RuleAction.IP_CIDR,
RuleAction.IP_CIDR6,
RuleAction.IP_SUFFIX,
RuleAction.RULE_SET,
].contains(this);
}
enum OverrideRuleType {
override,
added,
}
enum RuleTarget {
DIRECT,
REJECT,
}

View File

@@ -4,21 +4,20 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
class AccessFragment extends ConsumerStatefulWidget {
class AccessFragment extends StatefulWidget {
const AccessFragment({super.key});
@override
ConsumerState<AccessFragment> createState() => _AccessFragmentState();
State<AccessFragment> createState() => _AccessFragmentState();
}
class _AccessFragmentState extends ConsumerState<AccessFragment> {
class _AccessFragmentState extends State<AccessFragment> {
List<String> acceptList = [];
List<String> rejectList = [];
late ScrollController _controller;
@@ -29,11 +28,10 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
_updateInitList();
_controller = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appState;
final appState = globalState.appController.appState;
if (appState.packages.isEmpty) {
Future.delayed(const Duration(milliseconds: 300), () async {
ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
appState.packages = await app?.getPackages() ?? [];
});
}
});
@@ -46,8 +44,9 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
}
_updateInitList() {
acceptList = globalState.config.vpnProps.accessControl.acceptList;
rejectList = globalState.config.vpnProps.accessControl.rejectList;
final accessControl = globalState.appController.config.accessControl;
acceptList = accessControl.acceptList;
rejectList = accessControl.rejectList;
}
Widget _buildSearchButton() {
@@ -60,13 +59,9 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
acceptList: acceptList,
rejectList: rejectList,
),
).then(
(_) => setState(
() {
).then((_) => setState(() {
_updateInitList();
},
),
);
}));
},
icon: const Icon(Icons.search),
);
@@ -82,29 +77,28 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
return IconButton(
tooltip: tooltip,
onPressed: () {
ref.read(vpnSettingProvider.notifier).updateState((state) {
final isAccept =
state.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
return switch (isAccept) {
true => state.copyWith.accessControl(
acceptList: [],
),
false => state.copyWith.accessControl(
rejectList: [],
),
};
} else {
return switch (isAccept) {
true => state.copyWith.accessControl(
acceptList: allValueList,
),
false => state.copyWith.accessControl(
rejectList: allValueList,
),
};
}
});
final config = globalState.appController.config;
final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: [],
),
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
icon: isSelectedAll
? const Icon(Icons.deselect)
@@ -112,247 +106,223 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
);
}
_intelligentSelected() async {
final appState = globalState.appState;
final config = globalState.config;
final accessControl = config.vpnProps.accessControl;
final packageNames = appState.packages
.where(
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
acceptList: acceptList,
rejectList: rejectList,
),
);
}
Widget _buildSettingButton() {
return IconButton(
onPressed: () async {
final res = await showSheet<int>(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
props: SheetProps(
isScrollControlled: true,
body: AccessControlWidget(
context: context,
),
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: AccessControlPanel(),
title: appLocalizations.proxiesSetting,
);
},
);
if (res == 1) {
_intelligentSelected();
}
},
icon: const Icon(Icons.tune),
);
}
_handleSelected(List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
return switch (
state.accessControl.mode == AccessControlMode.acceptSelected) {
true => state.copyWith.accessControl(
acceptList: valueList,
),
false => state.copyWith.accessControl(
rejectList: valueList,
),
};
});
}
@override
Widget build(BuildContext context) {
final state = ref.watch(packageListSelectorStateProvider);
final accessControl = state.accessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final currentList = accessControl.currentList;
final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe = accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 0,
child: ListItem.switchItem(
title: Text(appLocalizations.appAccessControl),
delegate: SwitchDelegate(
value: accessControl.enable,
onChanged: (enable) {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
enable: enable,
),
);
},
return Selector<Config, bool>(
selector: (_, config) => config.isAccessControl,
builder: (_, isAccessControl, child) {
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 0,
child: ListItem.switchItem(
title: Text(appLocalizations.appAccessControl),
delegate: SwitchDelegate(
value: isAccessControl,
onChanged: (isAccessControl) {
final config = context.read<Config>();
config.isAccessControl = isAccessControl;
},
),
),
),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(
height: 12,
),
),
Flexible(
child: DisabledMask(
status: !accessControl.enable,
child: Column(
children: [
ActivateBox(
active: accessControl.enable,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(
height: 12,
),
),
Flexible(
child: child!,
),
],
);
},
child: Selector<AppState, List<Package>>(
selector: (_, appState) => appState.packages,
builder: (_, packages, ___) {
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
packages: appState.packages,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final currentList = accessControl.currentList;
final packageNameList =
packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
ActivateBox(
active: isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
Flexible(
child: _buildSearchButton(),
),
Flexible(
child: _buildSelectedAllButton(
isSelectedAll: valueList.length ==
packageNameList.length,
allValueList: packageNameList,
),
),
Flexible(
child: Text(describe),
)
child: _buildSettingButton(),
),
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(),
),
Flexible(
child: _buildSelectedAllButton(
isSelectedAll:
valueList.length == packageNameList.length,
allValueList: packageNameList,
),
),
Flexible(
child: _buildSettingButton(),
),
],
),
],
),
),
),
),
Expanded(
flex: 1,
child: packages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: CommonScrollBar(
controller: _controller,
child: ListView.builder(
controller: _controller,
itemCount: packages.length,
itemExtent: 72,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList.contains(package.packageName),
isActive: accessControl.enable,
onChanged: (value) {
_handleSelected(valueList, package, value);
Expanded(
flex: 1,
child: packages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: CommonScrollBar(
controller: _controller,
child: ListView.builder(
controller: _controller,
itemCount: packages.length,
itemExtent: 72,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value:
valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
);
},
),
),
),
),
),
],
),
],
),
),
),
],
);
},
);
},
),
);
}
}
@@ -373,47 +343,45 @@ class PackageListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FadeScaleEnterBox(
child: ActivateBox(
active: isActive,
child: ListItem.checkbox(
leading: SizedBox(
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: app?.getPackageIcon(package.packageName),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
return ActivateBox(
active: isActive,
child: ListItem.checkbox(
leading: SizedBox(
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: app?.getPackageIcon(package.packageName),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
title: Text(
package.label,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
title: Text(
package.label,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
package.packageName,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
delegate: CheckboxDelegate(
value: value,
onChanged: onChanged,
maxLines: 1,
),
subtitle: Text(
package.packageName,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
delegate: CheckboxDelegate(
value: value,
onChanged: onChanged,
),
),
);
@@ -458,31 +426,15 @@ class AccessControlSearchDelegate extends SearchDelegate {
);
}
_handleSelected(
WidgetRef ref, List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
return switch (
state.accessControl.mode == AccessControlMode.acceptSelected) {
true => state.copyWith.accessControl(
acceptList: valueList,
),
false => state.copyWith.accessControl(
rejectList: valueList,
),
};
});
}
Widget _packageList() {
final lowQuery = query.toLowerCase();
return Consumer(
builder: (context, ref, __) {
final state = ref.watch(packageListSelectorStateProvider);
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
packages: appState.packages,
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
@@ -497,7 +449,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
package.packageName.contains(lowQuery),
)
.toList();
final isAccessControl = state.accessControl.enable;
final isAccessControl = state.isAccessControl;
final currentList = accessControl.currentList;
final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
@@ -513,12 +465,21 @@ class AccessControlSearchDelegate extends SearchDelegate {
value: valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
_handleSelected(
ref,
valueList,
package,
value,
);
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config = globalState.appController.config;
if (accessControlMode == AccessControlMode.acceptSelected) {
config.accessControl = config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl = config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
@@ -539,16 +500,14 @@ class AccessControlSearchDelegate extends SearchDelegate {
}
}
class AccessControlPanel extends ConsumerStatefulWidget {
const AccessControlPanel({
class AccessControlWidget extends StatelessWidget {
final BuildContext context;
const AccessControlWidget({
super.key,
required this.context,
});
@override
ConsumerState createState() => _AccessControlPanelState();
}
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
return switch (mode) {
AccessControlMode.acceptSelected => Icons.adjust_outlined,
@@ -593,11 +552,9 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Consumer(
builder: (_, ref, __) {
final accessControlMode = ref.watch(
vpnSettingProvider.select((state) => state.accessControl.mode),
);
child: Selector<Config, AccessControlMode>(
selector: (_, config) => config.accessControl.mode,
builder: (_, accessControlMode, __) {
return Wrap(
spacing: 16,
children: [
@@ -609,11 +566,10 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
),
isSelected: accessControlMode == item,
onPressed: () {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
mode: item,
),
);
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
mode: item,
);
},
)
],
@@ -632,11 +588,9 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Consumer(
builder: (_, ref, __) {
final accessSortType = ref.watch(
vpnSettingProvider.select((state) => state.accessControl.sort),
);
child: Selector<Config, AccessSortType>(
selector: (_, config) => config.accessControl.sort,
builder: (_, accessSortType, __) {
return Wrap(
spacing: 16,
children: [
@@ -648,11 +602,10 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
),
isSelected: accessSortType == item,
onPressed: () {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
sort: item,
),
);
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
sort: item,
);
},
),
],
@@ -671,12 +624,9 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Consumer(
builder: (_, ref, __) {
final isFilterSystemApp = ref.watch(
vpnSettingProvider
.select((state) => state.accessControl.isFilterSystemApp),
);
child: Selector<Config, bool>(
selector: (_, config) => config.accessControl.isFilterSystemApp,
builder: (_, isFilterSystemApp, __) {
return Wrap(
spacing: 16,
children: [
@@ -685,11 +635,10 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
_getTextWithIsFilterSystemApp(item),
isSelected: isFilterSystemApp == item,
onPressed: () {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
isFilterSystemApp: item,
),
);
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
isFilterSystemApp: item,
);
},
)
],
@@ -701,35 +650,63 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
);
}
_intelligentSelected() async {
final appState = globalState.appController.appState;
final config = globalState.appController.config;
final accessControl = config.accessControl;
final packageNames = appState.packages
.where(
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
Navigator.of(context).pop();
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
config.accessControl = accessControl.copyWith(
acceptList: acceptList,
rejectList: rejectList,
);
}
_copyToClipboard() async {
await globalState.safeRun(() {
final data = globalState.config.vpnProps.accessControl.toJson();
final data = globalState.appController.config.accessControl.toJson();
Clipboard.setData(
ClipboardData(
text: json.encode(data),
),
);
});
if (!mounted) return;
if (!context.mounted) return;
Navigator.of(context).pop();
}
_pasteToClipboard() async {
await globalState.safeRun(
() async {
final data = await Clipboard.getData('text/plain');
final text = data?.text;
if (text == null) return;
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
accessControl: AccessControl.fromJson(
json.decode(text),
),
),
);
},
);
if (!mounted) return;
await globalState.safeRun(() async {
final config = globalState.appController.config;
final data = await Clipboard.getData('text/plain');
final text = data?.text;
if (text == null) return;
config.accessControl = AccessControl.fromJson(
json.decode(text),
);
});
if (!context.mounted) return;
Navigator.of(context).pop();
}
@@ -748,9 +725,7 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
CommonChip(
avatar: const Icon(Icons.auto_awesome),
label: appLocalizations.intelligentSelected,
onPressed: () {
Navigator.of(context).pop(1);
},
onPressed: _intelligentSelected,
),
CommonChip(
avatar: const Icon(Icons.paste),
@@ -771,19 +746,17 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildModeSetting(),
..._buildSortSetting(),
..._buildSourceSetting(),
..._buildActionSetting(),
],
),
return Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildModeSetting(),
..._buildSortSetting(),
..._buildSourceSetting(),
..._buildActionSetting(),
],
),
);
}

View File

@@ -1,258 +1,61 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/config.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:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class CloseConnectionsItem extends ConsumerWidget {
const CloseConnectionsItem({super.key});
class CloseConnectionsSwitch extends StatelessWidget {
const CloseConnectionsSwitch({super.key});
@override
Widget build(BuildContext context, ref) {
final closeConnections = ref.watch(
appSettingProvider.select((state) => state.closeConnections),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate(
value: closeConnections,
onChanged: (value) async {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
closeConnections: value,
),
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.closeConnections,
builder: (_, closeConnections, __) {
return ListItem.switchItem(
title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate(
value: closeConnections,
onChanged: (value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
closeConnections: value,
);
},
),
},
),
);
},
);
}
}
class UsageItem extends ConsumerWidget {
const UsageItem({super.key});
class UsageSwitch extends StatelessWidget {
const UsageSwitch({super.key});
@override
Widget build(BuildContext context, ref) {
final onlyStatisticsProxy = ref.watch(
appSettingProvider.select((state) => state.onlyStatisticsProxy),
);
return ListItem.switchItem(
title: Text(appLocalizations.onlyStatisticsProxy),
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
delegate: SwitchDelegate(
value: onlyStatisticsProxy,
onChanged: (bool value) async {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
onlyStatisticsProxy: value,
),
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.onlyStatisticsProxy,
builder: (_, onlyProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.onlyStatisticsProxy),
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
delegate: SwitchDelegate(
value: onlyProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
onlyStatisticsProxy: value,
);
},
),
);
}
}
class MinimizeItem extends ConsumerWidget {
const MinimizeItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final minimizeOnExit = ref.watch(
appSettingProvider.select((state) => state.minimizeOnExit),
);
return ListItem.switchItem(
title: Text(appLocalizations.minimizeOnExit),
subtitle: Text(appLocalizations.minimizeOnExitDesc),
delegate: SwitchDelegate(
value: minimizeOnExit,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
minimizeOnExit: value,
),
);
},
),
);
}
}
class AutoLaunchItem extends ConsumerWidget {
const AutoLaunchItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoLaunch = ref.watch(
appSettingProvider.select((state) => state.autoLaunch),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoLaunch: value,
),
);
},
),
);
}
}
class SilentLaunchItem extends ConsumerWidget {
const SilentLaunchItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final silentLaunch = ref.watch(
appSettingProvider.select((state) => state.silentLaunch),
);
return ListItem.switchItem(
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
silentLaunch: value,
),
);
},
),
);
}
}
class AutoRunItem extends ConsumerWidget {
const AutoRunItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoRun = ref.watch(
appSettingProvider.select((state) => state.autoRun),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoRun: value,
),
);
},
),
);
}
}
class HiddenItem extends ConsumerWidget {
const HiddenItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final hidden = ref.watch(
appSettingProvider.select((state) => state.hidden),
);
return ListItem.switchItem(
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: hidden,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
hidden: value,
),
);
},
),
);
}
}
class AnimateTabItem extends ConsumerWidget {
const AnimateTabItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isAnimateToPage = ref.watch(
appSettingProvider.select((state) => state.isAnimateToPage),
);
return ListItem.switchItem(
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
isAnimateToPage: value,
),
);
},
),
);
}
}
class OpenLogsItem extends ConsumerWidget {
const OpenLogsItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final openLogs = ref.watch(
appSettingProvider.select((state) => state.openLogs),
);
return ListItem.switchItem(
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
openLogs: value,
),
);
},
),
);
}
}
class AutoCheckUpdateItem extends ConsumerWidget {
const AutoCheckUpdateItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoCheckUpdate = ref.watch(
appSettingProvider.select((state) => state.autoCheckUpdate),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoCheckUpdate: value,
),
);
},
),
},
),
);
},
);
}
}
@@ -268,20 +71,156 @@ class ApplicationSettingFragment extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<Widget> items = [
MinimizeItem(),
if (system.isDesktop) ...[
AutoLaunchItem(),
SilentLaunchItem(),
],
AutoRunItem(),
if (Platform.isAndroid) ...[
HiddenItem(),
],
AnimateTabItem(),
OpenLogsItem(),
CloseConnectionsItem(),
UsageItem(),
AutoCheckUpdateItem(),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.minimizeOnExit,
builder: (_, isMinimizeOnExit, child) {
return ListItem.switchItem(
title: Text(appLocalizations.minimizeOnExit),
subtitle: Text(appLocalizations.minimizeOnExitDesc),
delegate: SwitchDelegate(
value: isMinimizeOnExit,
onChanged: (bool value) {
final config = context.read<Config>();
config.appSetting = config.appSetting.copyWith(
minimizeOnExit: value,
);
},
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoLaunch,
builder: (_, autoLaunch, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoLaunch: value,
);
},
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.silentLaunch,
builder: (_, silentLaunch, child) {
return ListItem.switchItem(
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
silentLaunch: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoRun,
builder: (_, autoRun, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoRun: value,
);
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.hidden,
builder: (_, isExclude, child) {
return ListItem.switchItem(
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: isExclude,
onChanged: (value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
hidden: value,
);
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.isAnimateToPage,
builder: (_, isAnimateToPage, child) {
return ListItem.switchItem(
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
isAnimateToPage: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.openLogs,
builder: (_, openLogs, child) {
return ListItem.switchItem(
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
openLogs: value,
);
},
),
);
},
),
const CloseConnectionsSwitch(),
const UsageSwitch(),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoCheckUpdate,
builder: (_, autoCheckUpdate, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoCheckUpdate: value,
);
},
),
);
},
),
];
return ListView.separated(
itemBuilder: (_, index) {

View File

@@ -4,16 +4,14 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/dav_client.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
class BackupAndRecovery extends ConsumerWidget {
class BackupAndRecovery extends StatelessWidget {
const BackupAndRecovery({super.key});
_showAddWebDAV(DAV? dav) async {
@@ -123,140 +121,139 @@ class BackupAndRecovery extends ConsumerWidget {
_recoveryOnLocal(context, recoveryOption);
}
_handleChange(String? value, WidgetRef ref) {
if (value == null) {
return;
}
ref.read(appDAVSettingProvider.notifier).updateState(
(state) => state?.copyWith(
fileName: value,
),
);
}
@override
Widget build(BuildContext context, ref) {
final dav = ref.watch(appDAVSettingProvider);
final client = dav != null ? DAVClient(dav) : null;
return ListView(
children: [
ListHeader(title: appLocalizations.remote),
if (dav == null)
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,
),
),
)
else ...[
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: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeThroughBox(
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,
),
),
);
},
Widget build(BuildContext context) {
return Selector<Config, DAV?>(
selector: (_, config) => config.dav,
builder: (_, dav, __) {
final client = dav != null ? DAVClient(dav) : null;
return ListView(
children: [
ListHeader(title: appLocalizations.remote),
if (dav == null)
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,
),
],
),
)
else ...[
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: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeBox(
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,
const SizedBox(
height: 4,
),
),
),
const SizedBox(
height: 4,
),
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (value) {
_handleChange(value, ref);
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.appController.config.dav =
globalState.appController.config.dav?.copyWith(
fileName: value,
);
},
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
);
},
);
}
}
@@ -276,42 +273,45 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
@override
Widget build(BuildContext context) {
return CommonDialog(
title: appLocalizations.recovery,
padding: const EdgeInsets.symmetric(
return AlertDialog(
title: Text(appLocalizations.recovery),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),
)
],
content: SizedBox(
width: 250,
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),
)
],
),
),
);
}
}
class WebDAVFormDialog extends ConsumerStatefulWidget {
class WebDAVFormDialog extends StatefulWidget {
final DAV? dav;
const WebDAVFormDialog({super.key, this.dav});
@override
ConsumerState<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
State<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
}
class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
late TextEditingController uriController;
late TextEditingController userController;
late TextEditingController passwordController;
@@ -328,7 +328,7 @@ class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
_submit() {
if (!_formKey.currentState!.validate()) return;
ref.read(appDAVSettingProvider.notifier).value = DAV(
globalState.appController.config.dav = DAV(
uri: uriController.text,
user: userController.text,
password: passwordController.text,
@@ -337,7 +337,7 @@ class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
}
_delete() {
ref.read(appDAVSettingProvider.notifier).value = null;
globalState.appController.config.dav = null;
Navigator.pop(context);
}
@@ -349,8 +349,78 @@ class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
@override
Widget build(BuildContext context) {
return CommonDialog(
title: appLocalizations.webDAVConfiguration,
return AlertDialog(
title: Text(appLocalizations.webDAVConfiguration),
content: Form(
key: _formKey,
child: SizedBox(
width: dialogCommonWidth,
child: Wrap(
runSpacing: 16,
children: [
TextFormField(
controller: uriController,
maxLines: 5,
minLines: 1,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link),
border: const OutlineInputBorder(),
labelText: appLocalizations.address,
helperText: appLocalizations.addressHelp,
),
validator: (String? value) {
if (value == null || value.isEmpty || !value.isUrl) {
return appLocalizations.addressTip;
}
return null;
},
),
TextFormField(
controller: userController,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_circle),
border: const OutlineInputBorder(),
labelText: appLocalizations.account,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.accountTip;
}
return null;
},
),
ValueListenableBuilder(
valueListenable: _obscureController,
builder: (_, obscure, __) {
return TextFormField(
controller: passwordController,
obscureText: obscure,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
obscure ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
_obscureController.value = !obscure;
},
),
labelText: appLocalizations.password,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.passwordTip;
}
return null;
},
);
},
),
],
),
),
),
actions: [
if (widget.dav != null)
TextButton(
@@ -362,73 +432,6 @@ class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
child: Text(appLocalizations.save),
)
],
child: Form(
key: _formKey,
child: Wrap(
runSpacing: 16,
children: [
TextFormField(
controller: uriController,
maxLines: 5,
minLines: 1,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link),
border: const OutlineInputBorder(),
labelText: appLocalizations.address,
helperText: appLocalizations.addressHelp,
),
validator: (String? value) {
if (value == null || value.isEmpty || !value.isUrl) {
return appLocalizations.addressTip;
}
return null;
},
),
TextFormField(
controller: userController,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_circle),
border: const OutlineInputBorder(),
labelText: appLocalizations.account,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.accountTip;
}
return null;
},
),
ValueListenableBuilder(
valueListenable: _obscureController,
builder: (_, obscure, __) {
return TextFormField(
controller: passwordController,
obscureText: obscure,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
obscure ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
_obscureController.value = !obscure;
},
),
labelText: appLocalizations.password,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.passwordTip;
}
return null;
},
);
},
),
],
),
),
);
}
}

View File

@@ -2,13 +2,8 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/config/dns.dart';
import 'package:fl_clash/fragments/config/general.dart';
import 'package:fl_clash/fragments/config/network.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/providers/config.dart' show patchClashConfigProvider;
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../state.dart';
class ConfigFragment extends StatefulWidget {
const ConfigFragment({super.key});
@@ -21,6 +16,18 @@ class _ConfigFragmentState extends State<ConfigFragment> {
@override
Widget build(BuildContext context) {
List<Widget> items = [
ListItem.open(
title: Text(appLocalizations.network),
subtitle: Text(appLocalizations.networkDesc),
leading: const Icon(Icons.vpn_key),
delegate: OpenDelegate(
title: appLocalizations.network,
isScaffold: true,
isBlur: false,
extendPageWidth: 360,
widget: const NetworkListView(),
),
),
ListItem.open(
title: Text(appLocalizations.general),
subtitle: Text(appLocalizations.generalDesc),
@@ -30,52 +37,20 @@ class _ConfigFragmentState extends State<ConfigFragment> {
widget: generateListView(
generalItems,
),
blur: false,
),
),
ListItem.open(
title: Text(appLocalizations.network),
subtitle: Text(appLocalizations.networkDesc),
leading: const Icon(Icons.vpn_key),
delegate: OpenDelegate(
title: appLocalizations.network,
blur: false,
widget: const NetworkListView(),
isBlur: false,
extendPageWidth: 360,
),
),
ListItem.open(
title: const Text("DNS"),
subtitle: Text(appLocalizations.dnsDesc),
leading: const Icon(Icons.dns),
delegate: OpenDelegate(
delegate: const OpenDelegate(
title: "DNS",
action: Consumer(builder: (_, ref, __) {
return IconButton(
onPressed: () async {
final res = await globalState.showMessage(
title: appLocalizations.reset,
message: TextSpan(
text: appLocalizations.resetTip,
),
);
if (res != true) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
dns: defaultDns,
),
);
},
tooltip: appLocalizations.reset,
icon: const Icon(
Icons.replay,
),
);
}),
widget: const DnsListView(),
blur: false,
widget: DnsListView(),
isScaffold: true,
isBlur: false,
extendPageWidth: 360,
),
)
];

View File

@@ -1,196 +1,208 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/config.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:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
class OverrideItem extends ConsumerWidget {
class OverrideItem extends StatelessWidget {
const OverrideItem({super.key});
@override
Widget build(BuildContext context, ref) {
final override = ref.watch(overrideDnsProvider);
return ListItem.switchItem(
title: Text(appLocalizations.overrideDns),
subtitle: Text(appLocalizations.overrideDnsDesc),
delegate: SwitchDelegate(
value: override,
onChanged: (bool value) async {
ref.read(overrideDnsProvider.notifier).value = value;
},
),
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.overrideDns,
builder: (_, override, __) {
return ListItem.switchItem(
title: Text(appLocalizations.overrideDns),
subtitle: Text(appLocalizations.overrideDnsDesc),
delegate: SwitchDelegate(
value: override,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.overrideDns = value;
},
),
);
},
);
}
}
class StatusItem extends ConsumerWidget {
class StatusItem extends StatelessWidget {
const StatusItem({super.key});
@override
Widget build(BuildContext context, ref) {
final enable =
ref.watch(patchClashConfigProvider.select((state) => state.dns.enable));
return ListItem.switchItem(
title: Text(appLocalizations.status),
subtitle: Text(appLocalizations.statusDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(enable: value));
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: Text(appLocalizations.status),
subtitle: Text(appLocalizations.statusDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
enable: value,
);
},
),
);
},
);
}
}
class ListenItem extends ConsumerWidget {
const ListenItem({super.key});
@override
Widget build(BuildContext context, ref) {
final listen =
ref.watch(patchClashConfigProvider.select((state) => state.dns.listen));
return ListItem.input(
title: Text(appLocalizations.listen),
subtitle: Text(listen),
delegate: InputDelegate(
title: appLocalizations.listen,
value: listen,
onChanged: (String? value) {
if (value == null) {
return;
}
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(listen: value));
},
),
);
}
}
class PreferH3Item extends ConsumerWidget {
class PreferH3Item extends StatelessWidget {
const PreferH3Item({super.key});
@override
Widget build(BuildContext context, ref) {
final preferH3 = ref
.watch(patchClashConfigProvider.select((state) => state.dns.preferH3));
return ListItem.switchItem(
title: const Text("PreferH3"),
subtitle: Text(appLocalizations.preferH3Desc),
delegate: SwitchDelegate(
value: preferH3,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(preferH3: value));
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.preferH3,
builder: (_, preferH3, __) {
return ListItem.switchItem(
title: const Text("PreferH3"),
subtitle: Text(appLocalizations.preferH3Desc),
delegate: SwitchDelegate(
value: preferH3,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
preferH3: value,
);
},
),
);
},
);
}
}
class IPv6Item extends ConsumerWidget {
class IPv6Item extends StatelessWidget {
const IPv6Item({super.key});
@override
Widget build(BuildContext context, ref) {
final ipv6 = ref.watch(
patchClashConfigProvider.select((state) => state.dns.ipv6),
);
return ListItem.switchItem(
title: const Text("IPv6"),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(ipv6: value));
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
title: const Text("IPv6"),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
ipv6: value,
);
},
),
);
},
);
}
}
class RespectRulesItem extends ConsumerWidget {
class RespectRulesItem extends StatelessWidget {
const RespectRulesItem({super.key});
@override
Widget build(BuildContext context, ref) {
final respectRules = ref.watch(
patchClashConfigProvider.select((state) => state.dns.respectRules),
);
return ListItem.switchItem(
title: Text(appLocalizations.respectRules),
subtitle: Text(appLocalizations.respectRulesDesc),
delegate: SwitchDelegate(
value: respectRules,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(respectRules: value));
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.respectRules,
builder: (_, respectRules, __) {
return ListItem.switchItem(
title: Text(appLocalizations.respectRules),
subtitle: Text(appLocalizations.respectRulesDesc),
delegate: SwitchDelegate(
value: respectRules,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
respectRules: value,
);
},
),
);
},
);
}
}
class DnsModeItem extends ConsumerWidget {
class DnsModeItem extends StatelessWidget {
const DnsModeItem({super.key});
@override
Widget build(BuildContext context, ref) {
final enhancedMode = ref.watch(
patchClashConfigProvider.select((state) => state.dns.enhancedMode),
);
return ListItem<DnsMode>.options(
title: Text(appLocalizations.dnsMode),
subtitle: Text(enhancedMode.name),
delegate: OptionsDelegate(
title: appLocalizations.dnsMode,
options: DnsMode.values,
onChanged: (value) {
if (value == null) {
return;
}
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(enhancedMode: value));
},
textBuilder: (dnsMode) => dnsMode.name,
value: enhancedMode,
),
Widget build(BuildContext context) {
return Selector<ClashConfig, DnsMode>(
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
builder: (_, enhancedMode, __) {
return ListItem<DnsMode>.options(
title: Text(appLocalizations.dnsMode),
subtitle: Text(enhancedMode.name),
delegate: OptionsDelegate(
title: appLocalizations.dnsMode,
options: DnsMode.values,
onChanged: (value) {
if (value == null) {
return;
}
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(enhancedMode: value);
},
textBuilder: (dnsMode) => dnsMode.name,
value: enhancedMode,
),
);
},
);
}
}
class FakeIpRangeItem extends ConsumerWidget {
class FakeIpRangeItem extends StatelessWidget {
const FakeIpRangeItem({super.key});
@override
Widget build(BuildContext context, ref) {
final fakeIpRange = ref.watch(
patchClashConfigProvider.select((state) => state.dns.fakeIpRange),
);
return ListItem.input(
title: Text(appLocalizations.fakeipRange),
subtitle: Text(fakeIpRange),
delegate: InputDelegate(
title: appLocalizations.fakeipRange,
value: fakeIpRange,
onChanged: (String? value) {
if (value == null) {
return;
}
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(fakeIpRange: value));
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
builder: (_, fakeIpRange, __) {
return ListItem.input(
title: Text(appLocalizations.fakeipRange),
subtitle: Text(fakeIpRange),
delegate: InputDelegate(
title: appLocalizations.fakeipRange,
value: fakeIpRange,
onChanged: (String? value) {
if (value != null) {
try {
final clashConfig = globalState.appController.clashConfig;
clashConfig.dns = clashConfig.dns.copyWith(
fakeIpRange: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.fakeipRange,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
);
}
}
@@ -203,28 +215,27 @@ class FakeIpFilterItem extends StatelessWidget {
return ListItem.open(
title: Text(appLocalizations.fakeipFilter),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: appLocalizations.fakeipFilter,
widget: Consumer(
builder: (_, ref, __) {
final fakeIpFilter = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fakeIpFilter),
);
return ListInputPage(
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, fakeIpFilter, __) {
return ListPage(
title: appLocalizations.fakeipFilter,
items: fakeIpFilter,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(
fakeIpFilter: List.from(items),
));
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fakeIpFilter: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -239,26 +250,27 @@ class DefaultNameserverItem extends StatelessWidget {
title: Text(appLocalizations.defaultNameserver),
subtitle: Text(appLocalizations.defaultNameserverDesc),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: appLocalizations.defaultNameserver,
widget: Consumer(builder: (_, ref, __) {
final defaultNameserver = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.defaultNameserver),
);
return ListInputPage(
title: appLocalizations.defaultNameserver,
items: defaultNameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
defaultNameserver: List.from(items),
),
);
},
);
}),
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, defaultNameserver, __) {
return ListPage(
title: appLocalizations.defaultNameserver,
items: defaultNameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
defaultNameserver: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -274,71 +286,79 @@ class NameserverItem extends StatelessWidget {
subtitle: Text(appLocalizations.nameserverDesc),
delegate: OpenDelegate(
title: appLocalizations.nameserver,
blur: false,
widget: Consumer(builder: (_, ref, __) {
final nameserver = ref.watch(
patchClashConfigProvider.select((state) => state.dns.nameserver),
);
return ListInputPage(
title: appLocalizations.nameserver,
items: nameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
nameserver: List.from(items),
),
);
},
);
}),
isBlur: false,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.nameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, nameserver, __) {
return ListPage(
title: "域名服务器",
items: nameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserver: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class UseHostsItem extends ConsumerWidget {
class UseHostsItem extends StatelessWidget {
const UseHostsItem({super.key});
@override
Widget build(BuildContext context, ref) {
final useHosts = ref.watch(
patchClashConfigProvider.select((state) => state.dns.useHosts),
);
return ListItem.switchItem(
title: Text(appLocalizations.useHosts),
delegate: SwitchDelegate(
value: useHosts,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(useHosts: value));
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.useHosts,
builder: (_, useHosts, __) {
return ListItem.switchItem(
title: Text(appLocalizations.useHosts),
delegate: SwitchDelegate(
value: useHosts,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
useHosts: value,
);
},
),
);
},
);
}
}
class UseSystemHostsItem extends ConsumerWidget {
class UseSystemHostsItem extends StatelessWidget {
const UseSystemHostsItem({super.key});
@override
Widget build(BuildContext context, ref) {
final useSystemHosts = ref.watch(
patchClashConfigProvider.select((state) => state.dns.useSystemHosts),
);
return ListItem.switchItem(
title: Text(appLocalizations.useSystemHosts),
delegate: SwitchDelegate(
value: useSystemHosts,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(
useSystemHosts: value,
));
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
builder: (_, useSystemHosts, __) {
return ListItem.switchItem(
title: Text(appLocalizations.useSystemHosts),
delegate: SwitchDelegate(
value: useSystemHosts,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
useSystemHosts: value,
);
},
),
);
},
);
}
}
@@ -352,27 +372,29 @@ class NameserverPolicyItem extends StatelessWidget {
title: Text(appLocalizations.nameserverPolicy),
subtitle: Text(appLocalizations.nameserverPolicyDesc),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: appLocalizations.nameserverPolicy,
widget: Consumer(builder: (_, ref, __) {
final nameserverPolicy = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.nameserverPolicy),
);
return MapInputPage(
title: appLocalizations.nameserverPolicy,
map: nameserverPolicy,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
onChange: (value) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
nameserverPolicy: value,
),
);
},
);
}),
widget: Selector<ClashConfig, Map<String, String>>(
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, nameserverPolicy, __) {
return ListPage(
title: appLocalizations.nameserverPolicy,
items: nameserverPolicy.entries,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserverPolicy: Map.fromEntries(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -387,28 +409,27 @@ class ProxyServerNameserverItem extends StatelessWidget {
title: Text(appLocalizations.proxyNameserver),
subtitle: Text(appLocalizations.proxyNameserverDesc),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: appLocalizations.proxyNameserver,
widget: Consumer(
builder: (_, ref, __) {
final proxyServerNameserver = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.proxyServerNameserver),
);
return ListInputPage(
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, proxyServerNameserver, __) {
return ListPage(
title: appLocalizations.proxyNameserver,
items: proxyServerNameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
proxyServerNameserver: List.from(items),
),
);
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
proxyServerNameserver: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -423,81 +444,95 @@ class FallbackItem extends StatelessWidget {
title: Text(appLocalizations.fallback),
subtitle: Text(appLocalizations.fallbackDesc),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: appLocalizations.fallback,
widget: Consumer(builder: (_, ref, __) {
final fallback = ref.watch(
patchClashConfigProvider.select((state) => state.dns.fallback),
);
return ListInputPage(
title: appLocalizations.fallback,
items: fallback,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
fallback: List.from(items),
),
);
},
);
}),
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallback,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, fallback, __) {
return ListPage(
title: appLocalizations.fallback,
items: fallback,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallback: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class GeoipItem extends ConsumerWidget {
class GeoipItem extends StatelessWidget {
const GeoipItem({super.key});
@override
Widget build(BuildContext context, ref) {
final geoip = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.geoip),
);
return ListItem.switchItem(
title: const Text("Geoip"),
delegate: SwitchDelegate(
value: geoip,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns.fallbackFilter(
geoip: value,
));
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
builder: (_, geoip, __) {
return ListItem.switchItem(
title: const Text("Geoip"),
delegate: SwitchDelegate(
value: geoip,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
);
},
),
);
},
);
}
}
class GeoipCodeItem extends ConsumerWidget {
class GeoipCodeItem extends StatelessWidget {
const GeoipCodeItem({super.key});
@override
Widget build(BuildContext context, ref) {
final geoipCode = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.geoipCode),
);
return ListItem.input(
title: Text(appLocalizations.geoipCode),
subtitle: Text(geoipCode),
delegate: InputDelegate(
title: appLocalizations.geoipCode,
value: geoipCode,
onChanged: (String? value) {
if (value == null) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns.fallbackFilter(
geoipCode: value,
),
);
},
),
Widget build(BuildContext context) {
return Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
builder: (_, geoipCode, __) {
return ListItem.input(
title: Text(appLocalizations.geoipCode),
subtitle: Text(geoipCode),
delegate: InputDelegate(
title: appLocalizations.geoipCode,
value: geoipCode,
onChanged: (String? value) {
if (value != null) {
try {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geoipCode: value,
),
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.geoipCode,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
);
}
}
@@ -510,26 +545,29 @@ class GeositeItem extends StatelessWidget {
return ListItem.open(
title: const Text("Geosite"),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: "Geosite",
widget: Consumer(builder: (_, ref, __) {
final geosite = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.geosite),
);
return ListInputPage(
title: "Geosite",
items: geosite,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns.fallbackFilter(
geosite: List.from(items),
),
);
},
);
}),
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, geosite, __) {
return ListPage(
title: "Geosite",
items: geosite,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geosite: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -543,26 +581,29 @@ class IpcidrItem extends StatelessWidget {
return ListItem.open(
title: Text(appLocalizations.ipcidr),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: appLocalizations.ipcidr,
widget: Consumer(builder: (_, ref, ___) {
final ipcidr = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.ipcidr),
);
return ListInputPage(
title: appLocalizations.ipcidr,
items: ipcidr,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns.fallbackFilter(
ipcidr: List.from(items),
));
},
);
}),
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, ipcidr, __) {
return ListPage(
title: appLocalizations.ipcidr,
items: ipcidr,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
ipcidr: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -576,26 +617,29 @@ class DomainItem extends StatelessWidget {
return ListItem.open(
title: Text(appLocalizations.domain),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: appLocalizations.domain,
widget: Consumer(builder: (_, ref, __) {
final domain = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.domain),
);
return ListInputPage(
title: appLocalizations.domain,
items: domain,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns.fallbackFilter(
domain: List.from(items),
),
);
},
);
}),
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, domain, __) {
return ListPage(
title: appLocalizations.domain,
items: domain,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
domain: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -611,7 +655,6 @@ class DnsOptions extends StatelessWidget {
title: appLocalizations.options,
items: [
const StatusItem(),
const ListenItem(),
const UseHostsItem(),
const UseSystemHostsItem(),
const IPv6Item(),
@@ -657,11 +700,37 @@ const dnsItems = <Widget>[
FallbackFilterOptions(),
];
class DnsListView extends ConsumerWidget {
class DnsListView extends StatelessWidget {
const DnsListView({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context.commonScaffoldState?.actions = [
IconButton(
onPressed: () async {
final res = await globalState.showMessage(
title: appLocalizations.reset,
message: TextSpan(
text: appLocalizations.resetTip,
),
);
if (res != true) {
return;
}
globalState.appController.clashConfig.dns = defaultDns;
},
tooltip: appLocalizations.reset,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override
Widget build(BuildContext context, ref) {
Widget build(BuildContext context) {
_initActions(context);
return generateListView(
dnsItems,
);

View File

@@ -1,190 +1,198 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
class LogLevelItem extends ConsumerWidget {
class LogLevelItem extends StatelessWidget {
const LogLevelItem({super.key});
@override
Widget build(BuildContext context, ref) {
final logLevel =
ref.watch(patchClashConfigProvider.select((state) => state.logLevel));
return ListItem<LogLevel>.options(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(logLevel.name),
delegate: OptionsDelegate<LogLevel>(
title: appLocalizations.logLevel,
options: LogLevel.values,
onChanged: (LogLevel? value) {
if (value == null) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
logLevel: value,
),
);
},
textBuilder: (logLevel) => logLevel.name,
value: logLevel,
),
Widget build(BuildContext context) {
return Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem<LogLevel>.options(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
delegate: OptionsDelegate<LogLevel>(
title: appLocalizations.logLevel,
options: LogLevel.values,
onChanged: (LogLevel? value) {
if (value == null) {
return;
}
final appController = globalState.appController;
appController.clashConfig.logLevel = value;
},
textBuilder: (logLevel) => logLevel.name,
value: value,
),
);
},
);
}
}
class UaItem extends ConsumerWidget {
class UaItem extends StatelessWidget {
const UaItem({super.key});
@override
Widget build(BuildContext context, ref) {
final globalUa =
ref.watch(patchClashConfigProvider.select((state) => state.globalUa));
return ListItem<String?>.options(
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(globalUa ?? appLocalizations.defaultText),
delegate: OptionsDelegate<String?>(
title: "UA",
options: [
null,
"clash-verge/v1.6.6",
"ClashforWindows/0.19.23",
],
value: globalUa,
onChanged: (value) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
globalUa: value,
),
);
},
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
),
Widget build(BuildContext context) {
return Selector<ClashConfig, String?>(
selector: (_, clashConfig) => clashConfig.globalRealUa,
builder: (_, value, __) {
return ListItem<String?>.options(
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(value ?? appLocalizations.defaultText),
delegate: OptionsDelegate<String?>(
title: "UA",
options: [
null,
"clash-verge/v1.6.6",
"ClashforWindows/0.19.23",
],
value: value,
onChanged: (ua) {
final appController = globalState.appController;
appController.clashConfig.globalRealUa = ua;
},
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
),
);
},
);
}
}
class KeepAliveIntervalItem extends ConsumerWidget {
class KeepAliveIntervalItem extends StatelessWidget {
const KeepAliveIntervalItem({super.key});
@override
Widget build(BuildContext context, ref) {
final keepAliveInterval = ref.watch(
patchClashConfigProvider.select((state) => state.keepAliveInterval));
return ListItem.input(
leading: const Icon(Icons.timer_outlined),
title: Text(appLocalizations.keepAliveIntervalDesc),
subtitle: Text("$keepAliveInterval ${appLocalizations.seconds}"),
delegate: InputDelegate(
title: appLocalizations.keepAliveIntervalDesc,
suffixText: appLocalizations.seconds,
resetValue: "$defaultKeepAliveInterval",
value: "$keepAliveInterval",
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.safeRun(
() {
final intValue = int.parse(value);
if (intValue <= 0) {
throw "Invalid keepAliveInterval";
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
keepAliveInterval: intValue,
Widget build(BuildContext context) {
return Selector<ClashConfig, int>(
selector: (_, config) => config.keepAliveInterval,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.timer_outlined),
title: Text(appLocalizations.keepAliveIntervalDesc),
subtitle: Text("$value ${appLocalizations.seconds}"),
delegate: InputDelegate(
title: appLocalizations.keepAliveIntervalDesc,
suffixText: appLocalizations.seconds,
resetValue: "$defaultKeepAliveInterval",
value: "$value",
onChanged: (String? value) {
if (value != null) {
try {
final intValue = int.parse(value);
if (intValue <= 0) {
throw "Invalid keepAliveInterval";
}
globalState.appController.clashConfig.keepAliveInterval =
intValue;
} catch (e) {
globalState.showMessage(
title: appLocalizations.keepAliveIntervalDesc,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
silence: false,
title: appLocalizations.keepAliveIntervalDesc,
);
},
),
),
);
},
);
}
}
class TestUrlItem extends ConsumerWidget {
class TestUrlItem extends StatelessWidget {
const TestUrlItem({super.key});
@override
Widget build(BuildContext context, ref) {
final testUrl =
ref.watch(appSettingProvider.select((state) => state.testUrl));
return ListItem.input(
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(testUrl),
delegate: InputDelegate(
resetValue: defaultTestUrl,
title: appLocalizations.testUrl,
value: testUrl,
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.safeRun(
() {
if (!value.isUrl) {
throw "Invalid url";
Widget build(BuildContext context) {
return Selector<Config, String>(
selector: (_, config) => config.appSetting.testUrl,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(value),
delegate: InputDelegate(
resetValue: defaultTestUrl,
title: appLocalizations.testUrl,
value: value,
onChanged: (String? value) {
if (value != null) {
try {
if (!value.isUrl) {
throw "Invalid url";
}
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
testUrl: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
}
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
testUrl: value,
),
);
},
silence: false,
title: appLocalizations.testUrl,
);
}),
}
},
),
);
},
);
}
}
class MixedPortItem extends ConsumerWidget {
class MixedPortItem extends StatelessWidget {
const MixedPortItem({super.key});
@override
Widget build(BuildContext context, ref) {
final mixedPort =
ref.watch(patchClashConfigProvider.select((state) => state.mixedPort));
return ListItem.input(
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text("$mixedPort"),
delegate: InputDelegate(
title: appLocalizations.proxyPort,
value: "$mixedPort",
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.safeRun(
() {
final mixedPort = int.parse(value);
if (mixedPort < 1024 || mixedPort > 49151) {
throw "Invalid port";
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
mixedPort: mixedPort,
Widget build(BuildContext context) {
return Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text("$value"),
delegate: InputDelegate(
title: appLocalizations.proxyPort,
value: "$value",
onChanged: (String? value) {
if (value != null) {
try {
final mixedPort = int.parse(value);
if (mixedPort < 1024 || mixedPort > 49151) {
throw "Invalid port";
}
globalState.appController.clashConfig.mixedPort = mixedPort;
} catch (e) {
globalState.showMessage(
title: appLocalizations.proxyPort,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
silence: false,
title: appLocalizations.proxyPort,
);
},
resetValue: "$defaultMixedPort",
),
resetValue: "$defaultMixedPort",
),
);
},
);
}
}
@@ -199,218 +207,216 @@ class HostsItem extends StatelessWidget {
title: const Text("Hosts"),
subtitle: Text(appLocalizations.hostsDesc),
delegate: OpenDelegate(
blur: false,
isBlur: false,
title: "Hosts",
widget: Consumer(
builder: (_, ref, __) {
final hosts = ref
.watch(patchClashConfigProvider.select((state) => state.hosts));
return MapInputPage(
widget: Selector<ClashConfig, HostsMap>(
selector: (_, clashConfig) => clashConfig.hosts,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, hosts, ___) {
final entries = hosts.entries;
return ListPage(
title: "Hosts",
map: hosts,
items: entries,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
onChange: (value) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
hosts: value,
),
);
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
clashConfig.hosts = Map.fromEntries(items);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class Ipv6Item extends ConsumerWidget {
class Ipv6Item extends StatelessWidget {
const Ipv6Item({super.key});
@override
Widget build(BuildContext context, ref) {
final ipv6 =
ref.watch(patchClashConfigProvider.select((state) => state.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 {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
ipv6: value,
),
);
},
),
Widget build(BuildContext context) {
return 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;
},
),
);
},
);
}
}
class AllowLanItem extends ConsumerWidget {
class AllowLanItem extends StatelessWidget {
const AllowLanItem({super.key});
@override
Widget build(BuildContext context, ref) {
final allowLan =
ref.watch(patchClashConfigProvider.select((state) => state.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 {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
allowLan: value,
),
);
},
),
Widget build(BuildContext context) {
return 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;
},
),
);
},
);
}
}
class UnifiedDelayItem extends ConsumerWidget {
class UnifiedDelayItem extends StatelessWidget {
const UnifiedDelayItem({super.key});
@override
Widget build(BuildContext context, ref) {
final unifiedDelay = ref
.watch(patchClashConfigProvider.select((state) => state.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 {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
unifiedDelay: value,
),
);
},
),
Widget build(BuildContext context) {
return 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;
},
),
);
},
);
}
}
class FindProcessItem extends ConsumerWidget {
class FindProcessItem extends StatelessWidget {
const FindProcessItem({super.key});
@override
Widget build(BuildContext context, ref) {
final findProcess = ref.watch(patchClashConfigProvider
.select((state) => state.findProcessMode == FindProcessMode.always));
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 {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
findProcessMode:
value ? FindProcessMode.always : FindProcessMode.off,
),
);
},
),
Widget build(BuildContext context) {
return 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;
},
),
);
},
);
}
}
class TcpConcurrentItem extends ConsumerWidget {
class TcpConcurrentItem extends StatelessWidget {
const TcpConcurrentItem({super.key});
@override
Widget build(BuildContext context, ref) {
final tcpConcurrent = ref
.watch(patchClashConfigProvider.select((state) => state.tcpConcurrent));
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
tcpConcurrent: value,
),
);
},
),
Widget build(BuildContext context) {
return 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;
},
),
);
},
);
}
}
class GeodataLoaderItem extends ConsumerWidget {
class GeodataLoaderItem extends StatelessWidget {
const GeodataLoaderItem({super.key});
@override
Widget build(BuildContext context, ref) {
final isMemconservative = ref.watch(patchClashConfigProvider.select(
(state) => state.geodataLoader == GeodataLoader.memconservative));
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: isMemconservative,
onChanged: (bool value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
geodataLoader: value
? GeodataLoader.memconservative
: GeodataLoader.standard,
),
);
},
),
Widget build(BuildContext context) {
return 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;
},
),
);
},
);
}
}
class ExternalControllerItem extends ConsumerWidget {
class ExternalControllerItem extends StatelessWidget {
const ExternalControllerItem({super.key});
@override
Widget build(BuildContext context, ref) {
final hasExternalController = ref.watch(patchClashConfigProvider.select(
(state) => state.externalController == ExternalControllerStatus.open));
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 {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
externalController: value
? ExternalControllerStatus.open
: ExternalControllerStatus.close,
),
);
},
),
Widget build(BuildContext context) {
return 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 : '';
},
),
);
},
);
}
}
final generalItems = <Widget>[
final generalItems = const [
LogLevelItem(),
UaItem(),
if (system.isDesktop) KeepAliveIntervalItem(),
KeepAliveIntervalItem(),
TestUrlItem(),
MixedPortItem(),
HostsItem(),

View File

@@ -3,185 +3,200 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class VPNItem extends ConsumerWidget {
class VPNItem extends StatelessWidget {
const VPNItem({super.key});
@override
Widget build(BuildContext context, ref) {
final enable =
ref.watch(vpnSettingProvider.select((state) => state.enable));
return ListItem.switchItem(
title: const Text("VPN"),
subtitle: Text(appLocalizations.vpnEnableDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
enable: value,
),
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: const Text("VPN"),
subtitle: Text(appLocalizations.vpnEnableDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
final config = globalState.appController.config;
config.vpnProps = config.vpnProps.copyWith(
enable: value,
);
},
),
},
),
);
},
);
}
}
class TUNItem extends ConsumerWidget {
class TUNItem extends StatelessWidget {
const TUNItem({super.key});
@override
Widget build(BuildContext context, ref) {
final enable =
ref.watch(patchClashConfigProvider.select((state) => state.tun.enable));
return ListItem.switchItem(
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(
enable: value,
),
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
);
},
),
},
),
);
},
);
}
}
class AllowBypassItem extends ConsumerWidget {
class AllowBypassItem extends StatelessWidget {
const AllowBypassItem({super.key});
@override
Widget build(BuildContext context, ref) {
final allowBypass =
ref.watch(vpnSettingProvider.select((state) => state.allowBypass));
return ListItem.switchItem(
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
allowBypass: value,
),
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
allowBypass: value,
);
},
),
},
),
);
},
);
}
}
class VpnSystemProxyItem extends ConsumerWidget {
class VpnSystemProxyItem extends StatelessWidget {
const VpnSystemProxyItem({super.key});
@override
Widget build(BuildContext context, ref) {
final systemProxy =
ref.watch(vpnSettingProvider.select((state) => state.systemProxy));
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: value,
),
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
systemProxy: value,
);
},
),
},
),
);
},
);
}
}
class SystemProxyItem extends ConsumerWidget {
class SystemProxyItem extends StatelessWidget {
const SystemProxyItem({super.key});
@override
Widget build(BuildContext context, ref) {
final systemProxy =
ref.watch(networkSettingProvider.select((state) => state.systemProxy));
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: value,
),
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.networkProps.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
final networkProps = config.networkProps;
config.networkProps = networkProps.copyWith(
systemProxy: value,
);
},
),
},
),
);
},
);
}
}
class Ipv6Item extends ConsumerWidget {
class Ipv6Item extends StatelessWidget {
const Ipv6Item({super.key});
@override
Widget build(BuildContext context, ref) {
final ipv6 = ref.watch(vpnSettingProvider.select((state) => state.ipv6));
return ListItem.switchItem(
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6InboundDesc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
ipv6: value,
),
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6InboundDesc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
ipv6: value,
);
},
),
},
),
);
},
);
}
}
class TunStackItem extends ConsumerWidget {
class TunStackItem extends StatelessWidget {
const TunStackItem({super.key});
@override
Widget build(BuildContext context, ref) {
final stack =
ref.watch(patchClashConfigProvider.select((state) => state.tun.stack));
return ListItem.options(
title: Text(appLocalizations.stackMode),
subtitle: Text(stack.name),
delegate: OptionsDelegate<TunStack>(
value: stack,
options: TunStack.values,
textBuilder: (value) => value.name,
onChanged: (value) {
if (value == null) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(
stack: value,
),
Widget build(BuildContext context) {
return Selector<ClashConfig, TunStack>(
selector: (_, clashConfig) => clashConfig.tun.stack,
builder: (_, stack, __) {
return ListItem.options(
title: Text(appLocalizations.stackMode),
subtitle: Text(stack.name),
delegate: OptionsDelegate<TunStack>(
value: stack,
options: TunStack.values,
textBuilder: (value) => value.name,
onChanged: (value) {
if (value == null) {
return;
}
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
stack: value,
);
},
title: appLocalizations.stackMode,
),
},
title: appLocalizations.stackMode,
),
);
},
);
}
}
@@ -189,7 +204,7 @@ class TunStackItem extends ConsumerWidget {
class BypassDomainItem extends StatelessWidget {
const BypassDomainItem({super.key});
_initActions(BuildContext context, WidgetRef ref) {
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context.commonScaffoldState?.actions = [
IconButton(
@@ -203,11 +218,10 @@ class BypassDomainItem extends StatelessWidget {
if (res != true) {
return;
}
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
bypassDomain: defaultBypassDomain,
),
);
final config = globalState.appController.config;
config.networkProps = config.networkProps.copyWith(
bypassDomain: defaultBypassDomain,
);
},
tooltip: appLocalizations.reset,
icon: const Icon(
@@ -224,98 +238,103 @@ class BypassDomainItem extends StatelessWidget {
title: Text(appLocalizations.bypassDomain),
subtitle: Text(appLocalizations.bypassDomainDesc),
delegate: OpenDelegate(
blur: false,
isBlur: false,
isScaffold: true,
title: appLocalizations.bypassDomain,
widget: Consumer(
builder: (_, ref, __) {
_initActions(context, ref);
final bypassDomain = ref.watch(
networkSettingProvider.select((state) => state.bypassDomain));
return ListInputPage(
widget: Selector<Config, List<String>>(
selector: (_, config) => config.networkProps.bypassDomain,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (context, bypassDomain, __) {
_initActions(context);
return ListPage(
title: appLocalizations.bypassDomain,
items: bypassDomain,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
bypassDomain: List.from(items),
),
);
final config = globalState.appController.config;
config.networkProps = config.networkProps.copyWith(
bypassDomain: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class RouteModeItem extends ConsumerWidget {
class RouteModeItem extends StatelessWidget {
const RouteModeItem({super.key});
@override
Widget build(BuildContext context, ref) {
final routeMode =
ref.watch(networkSettingProvider.select((state) => state.routeMode));
return ListItem<RouteMode>.options(
title: Text(appLocalizations.routeMode),
subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
delegate: OptionsDelegate<RouteMode>(
title: appLocalizations.routeMode,
options: RouteMode.values,
onChanged: (RouteMode? value) {
if (value == null) {
return;
}
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
routeMode: value,
),
);
},
textBuilder: (routeMode) => Intl.message(
"routeMode_${routeMode.name}",
),
value: routeMode,
),
Widget build(BuildContext context) {
return Selector<ClashConfig, RouteMode>(
selector: (_, clashConfig) => clashConfig.routeMode,
builder: (_, value, __) {
return ListItem<RouteMode>.options(
title: Text(appLocalizations.routeMode),
subtitle: Text(Intl.message("routeMode_${value.name}")),
delegate: OptionsDelegate<RouteMode>(
title: appLocalizations.routeMode,
options: RouteMode.values,
onChanged: (RouteMode? value) {
if (value == null) {
return;
}
final appController = globalState.appController;
appController.clashConfig.routeMode = value;
},
textBuilder: (routeMode) => Intl.message(
"routeMode_${routeMode.name}",
),
value: value,
),
);
},
);
}
}
class RouteAddressItem extends ConsumerWidget {
class RouteAddressItem extends StatelessWidget {
const RouteAddressItem({super.key});
@override
Widget build(BuildContext context, ref) {
final bypassPrivate = ref.watch(networkSettingProvider
.select((state) => state.routeMode == RouteMode.bypassPrivate));
if (bypassPrivate) {
return Container();
}
return ListItem.open(
title: Text(appLocalizations.routeAddress),
subtitle: Text(appLocalizations.routeAddressDesc),
delegate: OpenDelegate(
blur: false,
maxWidth: 360,
title: appLocalizations.routeAddress,
widget: Consumer(
builder: (_, ref, __) {
final routeAddress = ref.watch(patchClashConfigProvider
.select((state) => state.tun.routeAddress));
return ListInputPage(
title: appLocalizations.routeAddress,
items: routeAddress,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(
routeAddress: List.from(items),
),
);
},
);
},
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config,
builder: (_, value, child) {
if (value) {
return child!;
}
return Container();
},
child: ListItem.open(
title: Text(appLocalizations.routeAddress),
subtitle: Text(appLocalizations.routeAddressDesc),
delegate: OpenDelegate(
isBlur: false,
isScaffold: true,
title: appLocalizations.routeAddress,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
shouldRebuild: (prev, next) =>
!stringListEquality.equals(prev, next),
builder: (context, routeAddress, __) {
return ListPage(
title: appLocalizations.routeAddress,
items: routeAddress,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
clashConfig.includeRouteAddress =
Set<String>.from(items).toList();
},
);
},
),
extendPageWidth: 360,
),
),
);
@@ -328,8 +347,7 @@ final networkItems = [
...generateSection(
title: "VPN",
items: [
const VpnSystemProxyItem(),
const BypassDomainItem(),
const SystemProxyItem(),
const AllowBypassItem(),
const Ipv6Item(),
],
@@ -353,10 +371,10 @@ final networkItems = [
),
];
class NetworkListView extends ConsumerWidget {
class NetworkListView extends StatelessWidget {
const NetworkListView({super.key});
_initActions(BuildContext context, WidgetRef ref) {
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context.commonScaffoldState?.actions = [
IconButton(
@@ -370,14 +388,9 @@ class NetworkListView extends ConsumerWidget {
if (res != true) {
return;
}
ref.read(vpnSettingProvider.notifier).updateState(
(state) => defaultVpnProps,
);
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
tun: defaultTun,
),
);
final appController = globalState.appController;
appController.config.vpnProps = defaultVpnProps;
appController.clashConfig.tun = defaultTun;
},
tooltip: appLocalizations.reset,
icon: const Icon(
@@ -389,8 +402,8 @@ class NetworkListView extends ConsumerWidget {
}
@override
Widget build(BuildContext context, ref) {
_initActions(context, ref);
Widget build(BuildContext context) {
_initActions(context);
return generateListView(
networkItems,
);

View File

@@ -4,23 +4,21 @@ import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
import 'item.dart';
class ConnectionsFragment extends ConsumerStatefulWidget {
class ConnectionsFragment extends StatefulWidget {
const ConnectionsFragment({super.key});
@override
ConsumerState<ConnectionsFragment> createState() =>
_ConnectionsFragmentState();
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
with PageMixin {
class _ConnectionsFragmentState extends State<ConnectionsFragment>
with ViewMixin {
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
const ConnectionsState(),
);
@@ -73,22 +71,17 @@ class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
@override
void initState() {
super.initState();
ref.listenManual(
isCurrentPageProvider(
PageLabel.connections,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
_updateConnections();
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
},
);
}
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
@@ -107,44 +100,56 @@ class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ConnectionsState>(
valueListenable: _connectionsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'connections' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return CommonScrollBar(
controller: _scrollController,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClickKeyword: (value) {
context.commonScaffoldState?.addKeyword(value);
},
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
);
return child!;
},
child: ValueListenableBuilder<ConnectionsState>(
valueListenable: _connectionsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
}
return CommonScrollBar(
controller: _scrollController,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
);
},
),
);
}
}

View File

@@ -4,20 +4,40 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
class ConnectionItem extends ConsumerWidget {
class FindProcessBuilder extends StatelessWidget {
final Widget Function(bool value) builder;
const FindProcessBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
builder: (_, value, __) {
return builder(value);
},
);
}
}
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClickKeyword;
final Function(String)? onClick;
final Widget? trailing;
const ConnectionItem({
super.key,
required this.connection,
this.onClickKeyword,
this.onClick,
this.trailing,
});
@@ -34,14 +54,7 @@ class ConnectionItem extends ConsumerWidget {
}
@override
Widget build(BuildContext context, ref) {
final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
),
);
Widget build(BuildContext context) {
final title = Text(
connection.desc,
style: context.textTheme.bodyLarge,
@@ -68,143 +81,70 @@ class ConnectionItem extends ConsumerWidget {
CommonChip(
label: chain,
onPressed: () {
if (onClickKeyword == null) return;
onClickKeyword!(chain);
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
);
return CommonPopupBox(
targetBuilder: (open) {
if (!Platform.isAndroid) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
title: title,
subtitle: subTitle,
trailing: trailing,
);
}
return FindProcessBuilder(
builder: (bool value) {
final leading = value
? GestureDetector(
onTap: () {
if (onClick == null) return;
final process = connection.metadata.process;
if (process.isEmpty) return;
onClick!(process);
},
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
)
: null;
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: value
? GestureDetector(
onTap: () {
if (onClickKeyword == null) return;
final process = connection.metadata.process;
if (process.isEmpty) return;
onClickKeyword!(process);
},
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
)
: null,
leading: leading,
title: title,
subtitle: subTitle,
trailing: trailing,
);
// return InkWell(
// child: GestureDetector(
// onLongPressStart: (details) {
// if (!system.isDesktop) {
// return;
// }
// open(
// offset: details.localPosition.translate(
// 0,
// -12,
// ),
// );
// },
// onSecondaryTapDown: (details) {
// if (!system.isDesktop) {
// return;
// }
// open(
// offset: details.localPosition.translate(
// 0,
// -12,
// ),
// );
// },
// child: ListItem(
// padding: const EdgeInsets.symmetric(
// horizontal: 16,
// vertical: 4,
// ),
// tileTitleAlignment: ListTileTitleAlignment.titleHeight,
// leading: value
// ? GestureDetector(
// onTap: () {
// if (onClickKeyword == null) return;
// final process = connection.metadata.process;
// if (process.isEmpty) return;
// onClickKeyword!(process);
// },
// child: Container(
// margin: const EdgeInsets.only(top: 4),
// width: 48,
// height: 48,
// child: FutureBuilder<ImageProvider?>(
// future: _getPackageIcon(connection),
// builder: (_, snapshot) {
// if (!snapshot.hasData && snapshot.data == null) {
// return Container();
// } else {
// return Image(
// image: snapshot.data!,
// gaplessPlayback: true,
// width: 48,
// height: 48,
// );
// }
// },
// ),
// ),
// )
// : null,
// title: title,
// subtitle: subTitle,
// trailing: trailing,
// ),
// ),
// onTap: () {},
// );
},
popup: CommonPopupMenu(
minWidth: 160,
items: [
PopupMenuItemData(
label: "编辑规则",
onPressed: () {
// _handleShowEditExtendPage(context);
},
),
PopupMenuItemData(
label: "设置直连",
onPressed: () {},
),
PopupMenuItemData(
label: "一键屏蔽",
onPressed: () {},
),
],
),
);
}
}

View File

@@ -1,28 +1,28 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
import 'item.dart';
double _preOffset = 0;
class RequestsFragment extends ConsumerStatefulWidget {
class RequestsFragment extends StatefulWidget {
const RequestsFragment({super.key});
@override
ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
State<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin {
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = [];
@@ -31,6 +31,8 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
@override
@@ -49,36 +51,26 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
@override
void initState() {
super.initState();
final appController = globalState.appController;
final appState = appController.appState;
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: globalState.appState.requests.list,
connections: appState.requests,
);
}
ref.listenManual(
isCurrentPageProvider(
PageLabel.requests,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
},
fireImmediately: true,
);
ref.listenManual(
requestsProvider.select((state) => state.list),
(prev, next) {
if (!connectionListEquality.equals(prev, next)) {
_requests = next;
updateRequestsThrottler();
}
},
fireImmediately: true,
);
}
double _calcCacheHeight(Connection item) {
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize(
Text(
item.desc,
@@ -99,13 +91,14 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
final lines = (chainSize.height / baseHeight).round();
final computerHeight =
size.height + chainSize.height + 24 + 24 * (lines - 1);
_cacheDynamicHeightMap.put(item.id, computerHeight);
return computerHeight;
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_key.currentState?.clearCache();
_cacheDynamicHeightMap.clear();
}
}
@@ -114,9 +107,26 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
_requestsStateNotifier.dispose();
_scrollController.dispose();
_currentMaxWidth = 0;
_cacheDynamicHeightMap.clear();
super.dispose();
}
Widget _wrapPage(Widget child) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: child,
);
}
updateRequestsThrottler() {
throttler.call("request", () {
final isEquality = connectionListEquality.equals(
@@ -134,92 +144,95 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
}, duration: commonDuration);
}
Widget _wrapRequestsUpdate(Widget child) {
return Selector<AppState, List<Connection>>(
selector: (_, appState) => appState.requests,
shouldRebuild: (prev, next) {
final isEquality = connectionListEquality.equals(prev, next);
if (!isEquality) {
_requests = next;
updateRequestsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return Consumer(
builder: (_, ref, child) {
final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
return FindProcessBuilder(builder: (value) {
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return _wrapPage(
_wrapRequestsUpdate(
ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
final items = connections
.map<Widget>(
(connection) => ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index, __) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
),
),
),
);
},
),
);
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return child!;
},
child: ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
final items = connections
.map<Widget>(
(connection) => ConnectionItem(
key: Key(connection.id),
connection: connection,
onClickKeyword: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: CacheItemExtentListView(
key: _key,
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
keyBuilder: (int index) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return "divider";
}
final connection = connections[(index / 2).floor()];
return connection.id;
},
),
),
),
);
},
),
);
),
);
});
},
);
}

View File

@@ -2,42 +2,31 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.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:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
import 'widgets/start_button.dart';
class DashboardFragment extends ConsumerStatefulWidget {
class DashboardFragment extends StatefulWidget {
const DashboardFragment({super.key});
@override
ConsumerState<DashboardFragment> createState() => _DashboardFragmentState();
State<DashboardFragment> createState() => _DashboardFragmentState();
}
class _DashboardFragmentState extends ConsumerState<DashboardFragment>
with PageMixin {
class _DashboardFragmentState extends State<DashboardFragment> {
final key = GlobalKey<SuperGridState>();
@override
initState() {
ref.listenManual(
isCurrentPageProvider(PageLabel.dashboard),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
return super.initState();
}
@override
Widget? get floatingActionButton => const StartButton();
@override
List<Widget> get actions => [
_initScaffold(bool isCurrent) {
if (!isCurrent) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.floatingActionButton = const StartButton();
commonScaffoldState?.actions = [
ValueListenableBuilder(
valueListenable: key.currentState!.addedChildrenNotifier,
builder: (_, addedChildren, child) {
@@ -78,59 +67,72 @@ class _DashboardFragmentState extends ConsumerState<DashboardFragment>
},
),
];
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
final dashboardWidgets = girdItems
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
);
});
}
@override
Widget build(BuildContext context) {
final dashboardState = ref.watch(dashboardStateProvider);
final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
...dashboardState.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
],
onSave: (girdItems) {
_handleSave(girdItems, ref);
},
addedItemsBuilder: (girdItems) {
return DashboardWidget.values
.where(
(item) =>
!girdItems.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
return ActiveBuilder(
label: "dashboard",
builder: (isCurrent, child) {
_initScaffold(isCurrent);
return child!;
},
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: Selector2<AppState, Config, DashboardState>(
selector: (_, appState, config) => DashboardState(
dashboardWidgets: config.appSetting.dashboardWidgets,
viewWidth: appState.viewWidth,
),
builder: (_, state, ___) {
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
return SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
...state.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
)
.map((item) => item.widget)
.toList();
},
],
onSave: (girdItems) {
final dashboardWidgets = girdItems
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
dashboardWidgets: dashboardWidgets,
);
},
addedItemsBuilder: (girdItems) {
return DashboardWidget.values
.where(
(item) =>
!girdItems.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map((item) => item.widget)
.toList();
},
);
},
),
),
),
);

View File

@@ -0,0 +1,58 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CoreInfo extends StatelessWidget {
const CoreInfo({super.key});
@override
Widget build(BuildContext context) {
return Selector<AppState, VersionInfo?>(
selector: (_, appState) => appState.versionInfo,
builder: (_, versionInfo, __) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.coreInfo,
iconData: Icons.memory,
),
child: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: Text(
versionInfo?.clashName ?? '',
style: context
.textTheme
.titleMedium
?.toSoftBold,
),
),
const SizedBox(
height: 8,
),
Flexible(
flex: 1,
child: Text(
versionInfo?.version ?? '',
style: context
.textTheme
.titleLarge
?.toSoftBold,
),
),
],
),
),
);
},
);
}
}

Some files were not shown because too many files have changed in this diff Show More