Compare commits

..

11 Commits

Author SHA1 Message Date
chen08209
c9cd80bcb3 Optimize android vpn performance
Add custom primary color and color scheme

Add linux nad windows arm release

Optimize requests and logs page
2025-04-18 16:54:05 +08:00
chen08209
a77b3a35e8 Fix map input page delete issues 2025-04-13 17:32:01 +08:00
chen08209
2d2708d7bd Update changelog 2025-04-08 07:55:45 +00:00
chen08209
ef5f6dbd59 Add rule override
Update core

Optimize more details
2025-04-08 15:35:14 +08:00
chen08209
b6c7b15e3e Update changelog 2025-03-10 10:53:24 +00:00
chen08209
de9c5ba9cc Optimize dashboard performance
Fix some issues
2025-03-10 18:41:42 +08:00
chen08209
2aae00cf68 Fix unselected proxy group delay issues 2025-03-10 18:41:42 +08:00
chen08209
68be2d34a1 Fix asn url issues 2025-03-08 04:22:32 +08:00
chen08209
7895ccf720 Update changelog 2025-03-07 16:03:59 +00:00
chen08209
e92900dbbd 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
2025-03-07 23:49:27 +08:00
chen08209
eada271c49 Update changelog 2025-03-05 07:22:18 +00:00
186 changed files with 13580 additions and 3613 deletions

View File

@@ -4,6 +4,8 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
env:
IS_STABLE: ${{ !contains(github.ref, '-') }}
jobs: jobs:
build: build:
@@ -25,29 +27,27 @@ jobs:
- platform: macos - platform: macos
os: macos-latest os: macos-latest
arch: arm64 arch: arm64
- platform: windows
os: windows-11-arm
arch: arm64
- platform: linux
os: ubuntu-24.04-arm
arch: arm64
steps: steps:
- name: Setup rust
if: startsWith(matrix.os, 'windows-11-arm')
run: |
Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe
.\rustup-init.exe -y --default-toolchain stable
$cargoPath = "$env:USERPROFILE\.cargo\bin"
Add-Content $env:GITHUB_PATH $cargoPath
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- name: Setup JAVA
if: startsWith(matrix.platform,'android')
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 17
- name: Setup NDK
if: startsWith(matrix.platform,'android')
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26b
add-to-path: true
link-to-sdk: true
- name: Setup Android Signing - name: Setup Android Signing
if: startsWith(matrix.platform,'android') if: startsWith(matrix.platform,'android')
run: | run: |
@@ -56,26 +56,24 @@ jobs:
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 'stable' go-version: '1.24.0'
cache-dependency-path: | cache-dependency-path: |
core/go.sum core/go.sum
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.24.5 channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
channel: stable
cache: true cache: true
- name: Get Flutter Dependency - name: Get Flutter Dependency
run: flutter pub get run: flutter pub get
- name: Setup - name: Setup
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
- name: Upload - name: Upload
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -89,14 +87,13 @@ jobs:
needs: [ build ] needs: [ build ]
steps: steps:
- name: Checkout - name: Checkout
if: ${{ !contains(github.ref, '+') }}
uses: actions/checkout@v4 uses: actions/checkout@v4
if: ${{ env.IS_STABLE == 'true' }}
with: with:
fetch-depth: 0 fetch-depth: 0
ref: refs/heads/main ref: refs/heads/main
- name: Generate - name: Generate
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
run: | run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate)) tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1) preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
@@ -128,7 +125,7 @@ jobs:
cat NEW_CHANGELOG.md > CHANGELOG.md cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit - name: Commit
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
run: | run: |
git add CHANGELOG.md git add CHANGELOG.md
if ! git diff --cached --quiet; then if ! git diff --cached --quiet; then
@@ -207,7 +204,7 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install requests pip install requests
python release.py python release_telegram.py
- name: Patch release.md - name: Patch release.md
run: | run: |
@@ -215,21 +212,21 @@ jobs:
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
- name: Release - name: Release
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ./dist/* files: ./dist/*
body_path: './release.md' body_path: './release.md'
- name: Create Fdroid Source Dir - name: Create Fdroid Source Dir
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
run: | run: |
mkdir -p ./tmp mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully" echo "Files copied successfully"
- name: Push to fdroid repo - name: Push to fdroid repo
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
uses: cpina/github-action-push-to-another-repository@v1.7.2 uses: cpina/github-action-push-to-another-repository@v1.7.2
env: env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
@@ -239,7 +236,7 @@ jobs:
destination-repository-name: FlClash-fdroid-repo destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]' user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com' user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: action-pr target-branch: main
commit-message: Update from ${{ github.ref_name }} commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/ target-directory: /tmp/

6
.gitmodules vendored
View File

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

View File

@@ -1,3 +1,49 @@
## 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 ## v0.8.77
- Optimize performance - Optimize performance

10
Makefile Normal file
View File

@@ -0,0 +1,10 @@
android_arm64:
dart ./setup.dart android --arch arm64
macos_arm64:
dart ./setup.dart macos --arch arm64
android_app:
dart ./setup.dart android
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,8 +1 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter:
rules:
analyzer:
plugins:
- custom_lint

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,15 +10,13 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.follow.clash.FlClashApplication import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.RunState import com.follow.clash.RunState
import com.follow.clash.core.Core
import com.follow.clash.extensions.awaitResult import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.getProtocol
import com.follow.clash.extensions.resolveDns import com.follow.clash.extensions.resolveDns
import com.follow.clash.models.Process
import com.follow.clash.models.StartForegroundParams import com.follow.clash.models.StartForegroundParams
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import com.follow.clash.services.BaseServiceInterface import com.follow.clash.services.BaseServiceInterface
@@ -41,10 +39,12 @@ import kotlin.concurrent.withLock
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel private lateinit var flutterMethodChannel: MethodChannel
private var flClashService: BaseServiceInterface? = null private var flClashService: BaseServiceInterface? = null
private lateinit var options: VpnOptions private var options: VpnOptions? = null
private var isBind: Boolean = false
private lateinit var scope: CoroutineScope private lateinit var scope: CoroutineScope
private var lastStartForegroundParams: StartForegroundParams? = null private var lastStartForegroundParams: StartForegroundParams? = null
private var timerJob: Job? = null private var timerJob: Job? = null
private val uidPageNameMap = mutableMapOf<Int, String>()
private val connectivity by lazy { private val connectivity by lazy {
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>() FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
@@ -52,6 +52,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
isBind = true
flClashService = when (service) { flClashService = when (service) {
is FlClashVpnService.LocalBinder -> service.getService() is FlClashVpnService.LocalBinder -> service.getService()
is FlClashService.LocalBinder -> service.getService() is FlClashService.LocalBinder -> service.getService()
@@ -61,6 +62,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
override fun onServiceDisconnected(arg: ComponentName) { override fun onServiceDisconnected(arg: ComponentName) {
isBind = false
flClashService = null flClashService = null
} }
} }
@@ -91,62 +93,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.success(true) result.success(true)
} }
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null && flClashService is FlClashVpnService) {
try {
(flClashService as FlClashVpnService).protect(fd)
result.success(true)
} catch (e: RuntimeException) {
result.success(false)
}
} 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 -> { else -> {
result.notImplemented() result.notImplemented()
} }
@@ -154,6 +100,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
fun handleStart(options: VpnOptions): Boolean { fun handleStart(options: VpnOptions): Boolean {
if (options.enable != this.options?.enable) {
this.flClashService = null
}
this.options = options this.options = options
when (options.enable) { when (options.enable) {
true -> handleStartVpn() true -> handleStartVpn()
@@ -163,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
private fun handleStartVpn() { private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin() GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
?.requestVpnPermission { handleStartService()
handleStartService() }
}
} }
fun requestGc() { fun requestGc() {
@@ -236,6 +184,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
private fun startForegroundJob() { private fun startForegroundJob() {
stopForegroundJob()
timerJob = CoroutineScope(Dispatchers.Main).launch { timerJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) { while (isActive) {
startForeground() startForeground()
@@ -257,26 +206,58 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runLock.withLock { GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START GlobalState.runState.value = RunState.START
val fd = flClashService?.start(options) val fd = flClashService?.start(options!!)
flutterMethodChannel.invokeMethod( Core.startTun(
"started", fd fd = fd ?: 0,
protect = this::protect,
resolverProcess = this::resolverProcess,
) )
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() { fun handleStop() {
GlobalState.runLock.withLock { GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP GlobalState.runState.value = RunState.STOP
stopForegroundJob() stopForegroundJob()
Core.stopTun()
flClashService?.stop() flClashService?.stop()
GlobalState.handleTryDestroy() GlobalState.handleTryDestroy()
} }
} }
private fun bindService() { private fun bindService() {
val intent = when (options.enable) { if (isBind) {
FlClashApplication.getAppContext().unbindService(connection)
}
val intent = when (options?.enable == true) {
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java) true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
false -> Intent(FlClashApplication.getAppContext(), FlClashService::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.PendingIntent
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
@@ -87,6 +87,7 @@ class FlClashService : Service(), BaseServiceInterface {
} }
} }
} }
private suspend fun getNotificationBuilder(): NotificationCompat.Builder { private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await() return notificationBuilderDeferred.await()
} }
@@ -100,7 +101,8 @@ class FlClashService : Service(), BaseServiceInterface {
} }
} }
@SuppressLint("ForegroundServiceType", "WrongConstant")
@SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) { override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
@@ -116,7 +118,11 @@ class FlClashService : Service(), BaseServiceInterface {
.setContentTitle(title) .setContentTitle(title)
.setContentText(content).build() .setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification)
}
} else { } else {
startForeground(notificationId, notification) startForeground(notificationId, notification)
} }

View File

@@ -6,7 +6,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.net.ProxyInfo import android.net.ProxyInfo
import android.net.VpnService import android.net.VpnService
import android.os.Binder import android.os.Binder
@@ -45,9 +45,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
addAddress(cidr.address, cidr.prefixLength) addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv4RouteAddress() val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) { if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i -> try {
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}") routeAddress.forEach { i ->
addRoute(i.address, i.prefixLength) Log.d(
"addRoute4",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("0.0.0.0", 0)
} }
} else { } else {
addRoute("0.0.0.0", 0) addRoute("0.0.0.0", 0)
@@ -58,9 +65,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
addAddress(cidr.address, cidr.prefixLength) addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress() val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) { if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i -> try {
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}") routeAddress.forEach { i ->
addRoute(i.address, i.prefixLength) Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
} }
} else { } else {
addRoute("::", 0) addRoute("::", 0)
@@ -165,7 +179,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
return notificationBuilderDeferred.await() return notificationBuilderDeferred.await()
} }
@SuppressLint("ForegroundServiceType", "WrongConstant") @SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) { override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
@@ -182,7 +196,11 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
.setContentText(content) .setContentText(content)
.build() .build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification)
}
} else { } else {
startForeground(notificationId, notification) startForeground(notificationId, notification)
} }

View File

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

1
android/core/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,65 @@
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"
}
}
val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
doFirst {
delete("src/main/jniLibs")
}
from("../../libclash/android")
into("src/main/jniLibs")
}
afterEvaluate {
tasks.named("preBuild") {
dependsOn(copyNativeLibs)
}
}
dependencies {
implementation("androidx.core:core-ktx:1.16.0")
}

View File

21
android/core/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# 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

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

View File

@@ -0,0 +1,45 @@
cmake_minimum_required(VERSION 3.22.1)
project("core")
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
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")
message("LIB_CLASH_PATH ${LIB_CLASH_PATH}")
if (EXISTS ${LIB_CLASH_PATH})
message("Found libclash.so for ABI ${ANDROID_ABI}")
add_compile_definitions(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 ()
message("Not found libclash.so for ABI ${ANDROID_ABI}")
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME})
endif ()

View File

@@ -0,0 +1,75 @@
#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;
}
#endif

View File

@@ -0,0 +1,70 @@
#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

@@ -0,0 +1,39 @@
#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

@@ -0,0 +1,50 @@
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

@@ -0,0 +1,9 @@
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.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
kotlin_version=1.9.22 kotlin_version=1.9.22
agp_version=8.2.1 agp_version=8.9.1

View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

@@ -35,8 +35,8 @@ func (action Action) getResult(data interface{}) []byte {
func handleAction(action *Action, result func(data interface{})) { func handleAction(action *Action, result func(data interface{})) {
switch action.Method { switch action.Method {
case initClashMethod: case initClashMethod:
data := action.Data.(string) paramsString := action.Data.(string)
result(handleInitClash(data)) result(handleInitClash(paramsString))
return return
case getIsInitMethod: case getIsInitMethod:
result(handleGetIsInit()) result(handleGetIsInit())

77
core/android_bride.go Normal file
View File

@@ -0,0 +1,77 @@
//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,7 +28,20 @@ import (
"sync" "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 ( var (
version = 0
isRunning = false isRunning = false
runLock sync.Mutex runLock sync.Mutex
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"} ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
@@ -160,9 +173,19 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
return prof return prof
} }
func genHosts(hosts, patchHosts map[string]any) { func attachHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts { for k, v := range patchHosts {
hosts[k] = v 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))
}
} }
} }
@@ -173,26 +196,25 @@ func trimArr(arr []string) (r []string) {
return return
} }
func overrideRules(rules *[]string) { func overrideRules(rules, patchRules []string) []string {
var target = "" target := ""
for _, line := range *rules { for _, line := range rules {
rule := trimArr(strings.Split(line, ",")) rule := trimArr(strings.Split(line, ","))
l := len(rule) if len(rule) != 2 {
if l != 2 { continue
return
} }
if strings.ToUpper(rule[0]) == "MATCH" { if strings.EqualFold(rule[0], "MATCH") {
target = rule[1] target = rule[1]
break break
} }
} }
if target == "" { if target == "" {
return return rules
} }
var rulesExt = lo.Map(ips, func(ip string, index int) string { rulesExt := lo.Map(ips, func(ip string, _ int) string {
return fmt.Sprintf("DOMAIN %s %s", ip, target) return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
}) })
*rules = append(rulesExt, *rules...) return append(append(rulesExt, patchRules...), rules...)
} }
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) { func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
@@ -226,15 +248,20 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
for idx := range targetConfig.ProxyGroup { for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = "" targetConfig.ProxyGroup[idx]["url"] = ""
} }
genHosts(targetConfig.Hosts, patchConfig.Hosts) attachHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns { if configParams.OverrideDns {
updatePatchDns(patchConfig.DNS)
targetConfig.DNS = patchConfig.DNS targetConfig.DNS = patchConfig.DNS
} else { } else {
if targetConfig.DNS.Enable == false { if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true targetConfig.DNS.Enable = true
} }
} }
overrideRules(&targetConfig.Rule) if configParams.OverrideRule {
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
} else {
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
}
} }
func patchConfig() { func patchConfig() {

View File

@@ -7,12 +7,18 @@ import (
"time" "time"
) )
type InitParams struct {
HomeDir string `json:"home-dir"`
Version int `json:"version"`
}
type ConfigExtendedParams struct { type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"` IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"` IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"` SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"` TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"` OverrideDns bool `json:"override-dns"`
OverrideRule bool `json:"override-rule"`
} }
type GenerateConfigParams struct { type GenerateConfigParams struct {
@@ -70,11 +76,7 @@ const (
stopLogMethod Method = "stopLog" stopLogMethod Method = "stopLog"
startListenerMethod Method = "startListener" startListenerMethod Method = "startListener"
stopListenerMethod Method = "stopListener" stopListenerMethod Method = "stopListener"
startTunMethod Method = "startTun"
stopTunMethod Method = "stopTun"
updateDnsMethod Method = "updateDns" updateDnsMethod Method = "updateDns"
setProcessMapMethod Method = "setProcessMap"
setFdMapMethod Method = "setFdMap"
setStateMethod Method = "setState" setStateMethod Method = "setState"
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions" getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime" getRunTimeMethod Method = "getRunTime"
@@ -108,20 +110,3 @@ func (message *Message) Json() (string, error) {
data, err := json.Marshal(message) data, err := json.Marshal(message)
return string(data), err 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

@@ -21,7 +21,7 @@ require (
github.com/coreos/go-iptables v0.8.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.2 // indirect github.com/ebitengine/purego v0.8.2 // indirect
github.com/enfein/mieru/v3 v3.11.2 // indirect github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
@@ -50,21 +50,22 @@ require (
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // 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/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.1 // indirect github.com/metacubex/chacha v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // 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/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // 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-20250119013740-2a19cce83925 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.4.5 // 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-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // 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/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
github.com/metacubex/utls v1.6.6 // indirect github.com/metacubex/utls v1.6.8-alpha.4 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // 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.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
@@ -74,7 +75,7 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/quic-go/qpack v0.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/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect github.com/sagernet/cors v1.2.1 // indirect

View File

@@ -28,8 +28,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo= github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw= github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= 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/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -97,14 +97,16 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 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 h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= 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 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= 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 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.1/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 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic= 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 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/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
@@ -117,16 +119,16 @@ github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJ
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= 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 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0= github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= 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 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-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= 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/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 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8= github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= 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/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 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
@@ -153,8 +155,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/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 h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= 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/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=

View File

@@ -34,9 +34,15 @@ var (
currentConfig *config.Config currentConfig *config.Config
) )
func handleInitClash(homeDirStr string) bool { func handleInitClash(paramsString string) bool {
var params = InitParams{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
return false
}
version = params.Version
if !isInit { if !isInit {
constant.SetHomeDir(homeDirStr) constant.SetHomeDir(params.HomeDir)
isInit = true isInit = true
} }
return isInit return isInit

View File

@@ -4,6 +4,7 @@ package main
import "C" import "C"
import ( import (
"context"
bridge "core/dart-bridge" bridge "core/dart-bridge"
"core/platform" "core/platform"
"core/state" "core/state"
@@ -11,121 +12,116 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun" "github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"net"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
"unsafe"
) )
type Fd struct { type TunHandler struct {
Id string `json:"id"` listener *sing_tun.Listener
Value int64 `json:"value"` callback unsafe.Pointer
limit *semaphore.Weighted
} }
type Process struct { func (t *TunHandler) close() {
Id string `json:"id"` _ = t.limit.Acquire(context.TODO(), 4)
Metadata *constant.Metadata `json:"metadata"` defer t.limit.Release(4)
} removeTunHook()
if t.listener != nil {
type ProcessMapItem struct { _ = t.listener.Close()
Id string `json:"id"`
Value string `json:"value"`
}
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{}),
} }
if t.callback != nil {
releaseObject(t.callback)
}
t.callback = nil
t.listener = nil
} }
func (m *InvokeManager) completer(id string, value string) { func (t *TunHandler) handleProtect(fd int) {
m.invokeMap.Store(id, value) _ = t.limit.Acquire(context.Background(), 1)
m.chanLock.Lock() defer t.limit.Release(1)
if ch, ok := m.chanMap[id]; ok {
close(ch) if t.listener == nil {
delete(m.chanMap, id) return
} }
m.chanLock.Unlock()
protect(t.callback, fd)
} }
func (m *InvokeManager) await(id string) string { func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
m.chanLock.Lock() _ = t.limit.Acquire(context.Background(), 1)
if _, ok := m.chanMap[id]; !ok { defer t.limit.Release(1)
m.chanMap[id] = make(chan struct{})
}
ch := m.chanMap[id]
m.chanLock.Unlock()
timeout := time.After(500 * time.Millisecond) if t.listener == nil {
select {
case <-ch:
res, ok := m.invokeMap.Load(id)
m.invokeMap.Delete(id)
if ok {
return res.(string)
} else {
return ""
}
case <-timeout:
m.completer(id, "")
return "" 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 ( var (
invokePort int64 = -1 tunLock sync.Mutex
tunListener *sing_tun.Listener runTime *time.Time
fdInvokeMap = NewInvokeManager() errBlocked = errors.New("blocked")
processInvokeMap = NewInvokeManager() tunHandler *TunHandler
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() { func handleStopTun() {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
removeSocketHook()
runTime = nil runTime = nil
if tunListener != nil { if tunHandler != nil {
log.Infoln("TUN close") tunHandler.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 { func handleGetRunTime() string {
if runTime == nil { if runTime == nil {
return "" return ""
@@ -133,83 +129,29 @@ func handleGetRunTime() string {
return strconv.FormatInt(runTime.UnixMilli(), 10) return strconv.FormatInt(runTime.UnixMilli(), 10)
} }
func handleSetProcessMap(params string) { func initTunHook() {
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 { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() { if platform.ShouldBlockConnection() {
return errBlocked return errBlocked
} }
return conn.Control(func(fd uintptr) { return conn.Control(func(fd uintptr) {
fdInt := int64(fd) tunHandler.handleProtect(int(fd))
id := utils.NewUUIDV1().String()
handleMarkSocket(Fd{
Id: id,
Value: fdInt,
})
fdInvokeMap.await(id)
}) })
} }
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) { process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil { src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork return "", process.ErrInvalidNetwork
} }
id := utils.NewUUIDV1().String() return tunHandler.handleResolveProcess(src, dst), nil
handleParseProcess(Process{
Id: id,
Metadata: metadata,
})
return processInvokeMap.await(id), nil
} }
} }
func removeTunHook() {
dialer.DefaultSocketHook = nil
process.DefaultPackageNameResolver = nil
}
func handleGetAndroidVpnOptions() string { func handleGetAndroidVpnOptions() string {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
@@ -250,16 +192,6 @@ func handleGetCurrentProfileName() string {
func nextHandle(action *Action, result func(data interface{})) bool { func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method { switch action.Method {
case startTunMethod:
data := action.Data.(string)
var fd int
_ = json.Unmarshal([]byte(data), &fd)
result(handleStartTun(fd))
return true
case stopTunMethod:
handleStopTun()
result(true)
return true
case getAndroidVpnOptionsMethod: case getAndroidVpnOptionsMethod:
result(handleGetAndroidVpnOptions()) result(handleGetAndroidVpnOptions())
return true return true
@@ -268,16 +200,6 @@ func nextHandle(action *Action, result func(data interface{})) bool {
handleUpdateDns(data) handleUpdateDns(data)
result(true) result(true)
return true return true
case setFdMapMethod:
fdId := action.Data.(string)
handleSetFdMap(fdId)
result(true)
return true
case setProcessMapMethod:
data := action.Data.(string)
handleSetProcessMap(data)
result(true)
return true
case getRunTimeMethod: case getRunTimeMethod:
result(handleGetRunTime()) result(handleGetRunTime())
return true return true
@@ -289,13 +211,13 @@ func nextHandle(action *Action, result func(data interface{})) bool {
} }
//export quickStart //export quickStart
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) { func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port) i := int64(port)
dir := C.GoString(dirChar) paramsString := C.GoString(initParamsChar)
bytes := []byte(C.GoString(paramsChar)) bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar) stateParams := C.GoString(stateParamsChar)
go func() { go func() {
res := handleInitClash(dir) res := handleInitClash(paramsString)
if res == false { if res == false {
bridge.SendToPort(i, "init error") bridge.SendToPort(i, "init error")
} }
@@ -305,9 +227,8 @@ func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, po
} }
//export startTUN //export startTUN
func startTUN(fd C.int) *C.char { func startTUN(fd C.int, callback unsafe.Pointer) bool {
f := int(fd) return handleStartTun(int(fd), callback)
return C.CString(handleStartTun(f))
} }
//export getRunTime //export getRunTime
@@ -320,12 +241,6 @@ func stopTun() {
handleStopTun() handleStopTun()
} }
//export setFdMap
func setFdMap(fdIdChar *C.char) {
fdId := C.GoString(fdIdChar)
handleSetFdMap(fdId)
}
//export getCurrentProfileName //export getCurrentProfileName
func getCurrentProfileName() *C.char { func getCurrentProfileName() *C.char {
return C.CString(handleGetCurrentProfileName()) return C.CString(handleGetCurrentProfileName())
@@ -347,12 +262,3 @@ func updateDns(s *C.char) {
dnsList := C.GoString(s) dnsList := C.GoString(s)
handleUpdateDns(dnsList) handleUpdateDns(dnsList)
} }
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
handleSetProcessMap(paramsString)
}

176
core/platform/procfs.go Normal file
View File

@@ -0,0 +1,176 @@
//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

@@ -7,7 +7,7 @@ import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart'; import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart'; import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@@ -43,16 +43,8 @@ class ApplicationState extends ConsumerState<Application> {
ColorScheme _getAppColorScheme({ ColorScheme _getAppColorScheme({
required Brightness brightness, required Brightness brightness,
int? primaryColor, int? primaryColor,
required ColorSchemes systemColorSchemes,
}) { }) {
if (primaryColor != null) { return ref.read(genColorSchemeProvider(brightness));
return ColorScheme.fromSeed(
seedColor: Color(primaryColor),
brightness: brightness,
);
} else {
return systemColorSchemes.getColorSchemeForBrightness(brightness);
}
} }
@override @override
@@ -61,16 +53,6 @@ class ApplicationState extends ConsumerState<Application> {
_autoUpdateGroupTask(); _autoUpdateGroupTask();
_autoUpdateProfilesTask(); _autoUpdateProfilesTask();
globalState.appController = AppController(context, ref); globalState.appController = AppController(context, ref);
globalState.measure = Measure.of(context);
ref.listenManual(themeSettingProvider.select((state) => state.fontFamily),
(prev, next) {
if (prev != next) {
globalState.measure = Measure.of(
context,
fontFamily: next.value,
);
}
}, fireImmediately: true);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext; final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) { if (currentContext != null) {
@@ -98,7 +80,7 @@ class ApplicationState extends ConsumerState<Application> {
}); });
} }
_buildPlatformWrap(Widget child) { _buildPlatformState(Widget child) {
if (system.isDesktop) { if (system.isDesktop) {
return WindowManager( return WindowManager(
child: TrayManager( child: TrayManager(
@@ -117,18 +99,7 @@ class ApplicationState extends ConsumerState<Application> {
); );
} }
_buildPage(Widget page) { _buildState(Widget child) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: page,
);
}
return VpnManager(
child: page,
);
}
_buildWrap(Widget child) {
return AppStateManager( return AppStateManager(
child: ClashManager( child: ClashManager(
child: ConnectivityManager( child: ConnectivityManager(
@@ -142,6 +113,25 @@ 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( _updateSystemColorSchemes(
ColorScheme? lightDynamic, ColorScheme? lightDynamic,
ColorScheme? darkDynamic, ColorScheme? darkDynamic,
@@ -157,8 +147,8 @@ class ApplicationState extends ConsumerState<Application> {
@override @override
Widget build(context) { Widget build(context) {
return _buildPlatformWrap( return _buildPlatformState(
_buildWrap( _buildState(
Consumer( Consumer(
builder: (_, ref, child) { builder: (_, ref, child) {
final locale = final locale =
@@ -168,6 +158,7 @@ class ApplicationState extends ConsumerState<Application> {
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic); _updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey, navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
@@ -176,41 +167,32 @@ class ApplicationState extends ConsumerState<Application> {
GlobalWidgetsLocalizations.delegate GlobalWidgetsLocalizations.delegate
], ],
builder: (_, child) { builder: (_, child) {
return MessageManager( return AppEnvManager(
child: LayoutBuilder( child: _buildPlatformApp(
builder: (_, container) { _buildApp(child!),
globalState.appController.updateViewWidth(
container.maxWidth,
);
return _buildPage(child!);
},
), ),
); );
}, },
scrollBehavior: BaseScrollBehavior(), scrollBehavior: BaseScrollBehavior(),
title: appName, title: appName,
locale: other.getLocaleForString(locale), locale: utils.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales, supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: themeProps.themeMode, themeMode: themeProps.themeMode,
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
fontFamily: themeProps.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme( colorScheme: _getAppColorScheme(
brightness: Brightness.light, brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: themeProps.primaryColor, primaryColor: themeProps.primaryColor,
), ),
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
fontFamily: themeProps.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme( colorScheme: _getAppColorScheme(
brightness: Brightness.dark, brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: themeProps.primaryColor, primaryColor: themeProps.primaryColor,
).toPrueBlack(themeProps.prueBlack), ).toPureBlack(themeProps.pureBlack),
), ),
home: child, home: child,
); );

View File

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

View File

@@ -2348,6 +2348,97 @@ class ClashFFI {
set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value; 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( void initNativeApiBridge(
ffi.Pointer<ffi.Void> api, ffi.Pointer<ffi.Void> api,
) { ) {
@@ -2443,28 +2534,14 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener'); _lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
late final _stopListener = _stopListenerPtr.asFunction<void Function()>(); 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( void quickStart(
ffi.Pointer<ffi.Char> dirChar, ffi.Pointer<ffi.Char> initParamsChar,
ffi.Pointer<ffi.Char> paramsChar, ffi.Pointer<ffi.Char> paramsChar,
ffi.Pointer<ffi.Char> stateParamsChar, ffi.Pointer<ffi.Char> stateParamsChar,
int port, int port,
) { ) {
return _quickStart( return _quickStart(
dirChar, initParamsChar,
paramsChar, paramsChar,
stateParamsChar, stateParamsChar,
port, port,
@@ -2479,19 +2556,21 @@ class ClashFFI {
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>, int)>(); ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> startTUN( int startTUN(
int fd, int fd,
ffi.Pointer<ffi.Void> callback,
) { ) {
return _startTUN( return _startTUN(
fd, fd,
callback,
); );
} }
late final _startTUNPtr = late final _startTUNPtr = _lookup<
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>( ffi.NativeFunction<GoUint8 Function(ffi.Int, ffi.Pointer<ffi.Void>)>>(
'startTUN'); 'startTUN');
late final _startTUN = late final _startTUN =
_startTUNPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>(); _startTUNPtr.asFunction<int Function(int, ffi.Pointer<ffi.Void>)>();
ffi.Pointer<ffi.Char> getRunTime() { ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime(); return _getRunTime();
@@ -2511,20 +2590,6 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun'); _lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>(); 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() { ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName(); return _getCurrentProfileName();
} }
@@ -2572,20 +2637,6 @@ class ClashFFI {
'updateDns'); 'updateDns');
late final _updateDns = late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>(); _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 { final class __mbstate_t extends ffi.Union {
@@ -3738,6 +3789,31 @@ typedef mode_t = __darwin_mode_t;
typedef __darwin_mode_t = __uint16_t; typedef __darwin_mode_t = __uint16_t;
typedef __uint16_t = ffi.UnsignedShort; typedef __uint16_t = ffi.UnsignedShort;
typedef Dart__uint16_t = int; 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 { final class GoInterface extends ffi.Struct {
external ffi.Pointer<ffi.Void> t; external ffi.Pointer<ffi.Void> t;
@@ -3758,6 +3834,8 @@ final class GoSlice extends ffi.Struct {
typedef GoInt = GoInt64; typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong; typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int; typedef DartGoInt64 = int;
typedef GoUint8 = ffi.UnsignedChar;
typedef DartGoUint8 = int;
const int __has_safe_buffers = 1; const int __has_safe_buffers = 1;
@@ -3973,6 +4051,8 @@ const int __MAC_15_0 = 150000;
const int __MAC_15_1 = 150100; const int __MAC_15_1 = 150100;
const int __MAC_15_2 = 150200;
const int __IPHONE_2_0 = 20000; const int __IPHONE_2_0 = 20000;
const int __IPHONE_2_1 = 20100; const int __IPHONE_2_1 = 20100;
@@ -4135,6 +4215,8 @@ const int __IPHONE_18_0 = 180000;
const int __IPHONE_18_1 = 180100; const int __IPHONE_18_1 = 180100;
const int __IPHONE_18_2 = 180200;
const int __WATCHOS_1_0 = 10000; const int __WATCHOS_1_0 = 10000;
const int __WATCHOS_2_0 = 20000; const int __WATCHOS_2_0 = 20000;
@@ -4233,6 +4315,8 @@ const int __WATCHOS_11_0 = 110000;
const int __WATCHOS_11_1 = 110100; const int __WATCHOS_11_1 = 110100;
const int __WATCHOS_11_2 = 110200;
const int __TVOS_9_0 = 90000; const int __TVOS_9_0 = 90000;
const int __TVOS_9_1 = 90100; const int __TVOS_9_1 = 90100;
@@ -4333,6 +4417,8 @@ const int __TVOS_18_0 = 180000;
const int __TVOS_18_1 = 180100; const int __TVOS_18_1 = 180100;
const int __TVOS_18_2 = 180200;
const int __BRIDGEOS_2_0 = 20000; const int __BRIDGEOS_2_0 = 20000;
const int __BRIDGEOS_3_0 = 30000; const int __BRIDGEOS_3_0 = 30000;
@@ -4389,6 +4475,8 @@ const int __BRIDGEOS_9_0 = 90000;
const int __BRIDGEOS_9_1 = 90100; const int __BRIDGEOS_9_1 = 90100;
const int __BRIDGEOS_9_2 = 90200;
const int __DRIVERKIT_19_0 = 190000; const int __DRIVERKIT_19_0 = 190000;
const int __DRIVERKIT_20_0 = 200000; const int __DRIVERKIT_20_0 = 200000;
@@ -4419,6 +4507,8 @@ const int __DRIVERKIT_24_0 = 240000;
const int __DRIVERKIT_24_1 = 240100; const int __DRIVERKIT_24_1 = 240100;
const int __DRIVERKIT_24_2 = 240200;
const int __VISIONOS_1_0 = 10000; const int __VISIONOS_1_0 = 10000;
const int __VISIONOS_1_1 = 10100; const int __VISIONOS_1_1 = 10100;
@@ -4429,6 +4519,8 @@ const int __VISIONOS_2_0 = 20000;
const int __VISIONOS_2_1 = 20100; 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_0 = 1000;
const int MAC_OS_X_VERSION_10_1 = 1010; const int MAC_OS_X_VERSION_10_1 = 1010;
@@ -4555,9 +4647,11 @@ const int MAC_OS_VERSION_15_0 = 150000;
const int MAC_OS_VERSION_15_1 = 150100; 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_MIN_REQUIRED = 150000;
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150100; const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150200;
const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1; const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1;

View File

@@ -7,7 +7,7 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
mixin ClashInterface { mixin ClashInterface {
Future<bool> init(String homeDir); Future<bool> init(InitParams params);
Future<bool> preload(); Future<bool> preload();
@@ -74,12 +74,10 @@ mixin AndroidClashInterface {
Future<bool> setProcessMap(ProcessMapItem item); Future<bool> setProcessMap(ProcessMapItem item);
Future<bool> stopTun(); // Future<bool> stopTun();
Future<bool> updateDns(String value); Future<bool> updateDns(String value);
Future<DateTime?> startTun(int fd);
Future<AndroidVpnOptions?> getAndroidVpnOptions(); Future<AndroidVpnOptions?> getAndroidVpnOptions();
Future<String> getCurrentProfileName(); Future<String> getCurrentProfileName();
@@ -153,7 +151,7 @@ abstract class ClashHandlerInterface with ClashInterface {
Duration? timeout, Duration? timeout,
FutureOr<T> Function()? onTimeout, FutureOr<T> Function()? onTimeout,
}) async { }) async {
final id = "${method.name}#${other.id}"; final id = "${method.name}#${utils.id}";
callbackCompleterMap[id] = Completer<T>(); callbackCompleterMap[id] = Completer<T>();
@@ -191,10 +189,10 @@ abstract class ClashHandlerInterface with ClashInterface {
} }
@override @override
Future<bool> init(String homeDir) { Future<bool> init(InitParams params) {
return invoke<bool>( return invoke<bool>(
method: ActionMethod.initClash, method: ActionMethod.initClash,
data: homeDir, data: json.encode(params),
); );
} }

View File

@@ -122,25 +122,12 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
); );
} }
@override // @override
Future<DateTime?> startTun(int fd) async { // Future<bool> stopTun() {
final res = await invoke<String>( // return invoke<bool>(
method: ActionMethod.startTun, // method: ActionMethod.stopTun,
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 @override
Future<AndroidVpnOptions?> getAndroidVpnOptions() async { Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
@@ -224,37 +211,12 @@ 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) { updateDns(String dns) {
final dnsChar = dns.toNativeUtf8().cast<Char>(); final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar); clashFFI.updateDns(dnsChar);
malloc.free(dnsChar); malloc.free(dnsChar);
} }
setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
setState(CoreState state) { setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>(); final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar); clashFFI.setState(stateChar);
@@ -305,17 +267,11 @@ class ClashLibHandler {
return true; return true;
} }
setFdMap(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.setFdMap(idChar);
malloc.free(idChar);
}
Future<String> quickStart( Future<String> quickStart(
String homeDir, InitParams initParams,
UpdateConfigParams updateConfigParams, UpdateConfigParams updateConfigParams,
CoreState state, CoreState state,
) { ) {
final completer = Completer<String>(); final completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
receiver.listen((message) { receiver.listen((message) {
@@ -325,17 +281,18 @@ class ClashLibHandler {
} }
}); });
final params = json.encode(updateConfigParams); final params = json.encode(updateConfigParams);
final initValue = json.encode(initParams);
final stateParams = json.encode(state); final stateParams = json.encode(state);
final homeChar = homeDir.toNativeUtf8().cast<Char>(); final initParamsChar = initValue.toNativeUtf8().cast<Char>();
final paramsChar = params.toNativeUtf8().cast<Char>(); final paramsChar = params.toNativeUtf8().cast<Char>();
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>(); final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
clashFFI.quickStart( clashFFI.quickStart(
homeChar, initParamsChar,
paramsChar, paramsChar,
stateParamsChar, stateParamsChar,
receiver.sendPort.nativePort, receiver.sendPort.nativePort,
); );
malloc.free(homeChar); malloc.free(initParamsChar);
malloc.free(paramsChar); malloc.free(paramsChar);
malloc.free(stateParamsChar); malloc.free(stateParamsChar);
return completer.future; return completer.future;

View File

@@ -1,28 +1,92 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension ColorExtension on Color { extension ColorExtension on Color {
Color get opacity80 {
Color get toLight { return withAlpha(204);
return withOpacity(0.8);
} }
Color get toLighter { Color get opacity60 {
return withOpacity(0.6); return withAlpha(153);
} }
Color get toSoft { Color get opacity50 {
return withOpacity(0.15); return withAlpha(128);
} }
Color get toLittle { Color get opacity38 {
return withOpacity(0.03); return withAlpha(97);
} }
Color darken([double amount = .1]) { Color get opacity30 {
assert(amount >= 0 && amount <= 1); return withAlpha(77);
final hsl = HSLColor.fromColor(this); }
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor(); Color get opacity15 {
return withAlpha(38);
}
Color get opacity10 {
return withAlpha(15);
}
Color get opacity3 {
return withAlpha(76);
}
Color get opacity0 {
return withAlpha(0);
}
int get value32bit {
return _floatToInt8(a) << 24 |
_floatToInt8(r) << 16 |
_floatToInt8(g) << 8 |
_floatToInt8(b) << 0;
}
int get alpha8bit => (0xff000000 & value32bit) >> 24;
int get red8bit => (0x00ff0000 & value32bit) >> 16;
int get green8bit => (0x0000ff00 & value32bit) >> 8;
int get blue8bit => (0x000000ff & value32bit) >> 0;
int _floatToInt8(double x) {
return (x * 255.0).round() & 0xff;
}
Color lighten([double amount = 10]) {
if (amount <= 0) return this;
if (amount > 100) return Colors.white;
final HSLColor hsl = this == const Color(0xFF000000)
? HSLColor.fromColor(this).withSaturation(0)
: HSLColor.fromColor(this);
return hsl
.withLightness(min(1, max(0, hsl.lightness + amount / 100)))
.toColor();
}
String get hex {
final value = toARGB32();
final red = (value >> 16) & 0xFF;
final green = (value >> 8) & 0xFF;
final blue = value & 0xFF;
return '#${red.toRadixString(16).padLeft(2, '0')}'
'${green.toRadixString(16).padLeft(2, '0')}'
'${blue.toRadixString(16).padLeft(2, '0')}'
.toUpperCase();
}
Color darken([final int amount = 10]) {
if (amount <= 0) return this;
if (amount > 100) return Colors.black;
final HSLColor hsl = HSLColor.fromColor(this);
return hsl
.withLightness(min(1, max(0, hsl.lightness - amount / 100)))
.toColor();
} }
Color blendDarken( Color blendDarken(
@@ -51,11 +115,11 @@ extension ColorExtension on Color {
} }
extension ColorSchemeExtension on ColorScheme { extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
? copyWith( ? copyWith(
surface: Colors.black, surface: Colors.black,
surfaceContainer: surfaceContainer.darken( surfaceContainer: surfaceContainer.darken(
0.05, 5,
), ),
) )
: this; : this;

View File

@@ -19,7 +19,7 @@ export 'navigation.dart';
export 'navigator.dart'; export 'navigator.dart';
export 'network.dart'; export 'network.dart';
export 'num.dart'; export 'num.dart';
export 'other.dart'; export 'utils.dart';
export 'package.dart'; export 'package.dart';
export 'path.dart'; export 'path.dart';
export 'picker.dart'; export 'picker.dart';

View File

@@ -21,6 +21,11 @@ const baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16, vertical: 16,
horizontal: 16, horizontal: 16,
); );
double textScaleFactor = min(
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
1.2,
);
const httpTimeoutDuration = Duration(milliseconds: 5000); const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100); const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100);
@@ -46,7 +51,7 @@ const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600; const maxMobileWidth = 600;
const maxLaptopWidth = 840; const maxLaptopWidth = 840;
const defaultTestUrl = "https://www.gstatic.com/generate_204"; const defaultTestUrl = "https://www.gstatic.com/generate_204";
final filter = ImageFilter.blur( final commonFilter = ImageFilter.blur(
sigmaX: 5, sigmaX: 5,
sigmaY: 5, sigmaY: 5,
tileMode: TileMode.mirror, tileMode: TileMode.mirror,
@@ -73,12 +78,22 @@ const viewModeColumnsMap = {
ViewMode.desktop: [4, 3], ViewMode.desktop: [4, 3],
}; };
const defaultPrimaryColor = Colors.brown; const defaultPrimaryColor = 0xFF795548;
double getWidgetHeight(num lines) { double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0); return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0);
} }
final mainIsolate = "FlClashMainIsolate"; final mainIsolate = "FlClashMainIsolate";
final serviceIsolate = "FlClashServiceIsolate"; final serviceIsolate = "FlClashServiceIsolate";
const defaultPrimaryColors = [
defaultPrimaryColor,
0xFF03A9F4,
0xFFFFFF00,
0XFFBBC9CC,
0XFFABD397,
0XFFD8C0C3,
0XFF665390,
];

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
class Debouncer { class Debouncer {
final Map<dynamic, Timer> _operations = {}; final Map<dynamic, Timer?> _operations = {};
call( call(
dynamic tag, dynamic tag,
@@ -28,14 +28,15 @@ class Debouncer {
cancel(dynamic tag) { cancel(dynamic tag) {
_operations[tag]?.cancel(); _operations[tag]?.cancel();
_operations[tag] = null;
} }
} }
class Throttler { class Throttler {
final Map<dynamic, Timer> _operations = {}; final Map<dynamic, Timer?> _operations = {};
call( call(
String tag, dynamic tag,
Function func, { Function func, {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
@@ -60,6 +61,7 @@ class Throttler {
cancel(dynamic tag) { cancel(dynamic tag) {
_operations[tag]?.cancel(); _operations[tag]?.cancel();
_operations[tag] = null;
} }
} }

View File

@@ -1,24 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import '../state.dart';
class FlClashHttpOverrides extends HttpOverrides { 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 @override
HttpClient createHttpClient(SecurityContext? context) { HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context); final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true; client.findProxy = handleFindProxy;
client.findProxy = (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";
};
return client; return client;
} }
} }

View File

@@ -65,3 +65,12 @@ extension DoubleListExt on List<double> {
return -1; 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,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart'; import 'constant.dart';
@@ -34,6 +35,9 @@ class AutoLaunch {
} }
updateStatus(bool isAutoLaunch) async { updateStatus(bool isAutoLaunch) async {
if(kDebugMode){
return;
}
if (await isEnable == isAutoLaunch) return; if (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) { if (isAutoLaunch == true) {
enable(); enable();

View File

@@ -32,7 +32,7 @@ class FixedList<T> {
} }
class FixedMap<K, V> { class FixedMap<K, V> {
final int maxSize; int maxSize;
final Map<K, V> _map = {}; final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>(); final Queue<K> _queue = Queue<K>();
@@ -45,6 +45,7 @@ class FixedMap<K, V> {
} }
_map[key] = value; _map[key] = value;
_queue.add(key); _queue.add(key);
return value;
} }
clear() { clear() {
@@ -52,8 +53,13 @@ class FixedMap<K, V> {
_queue.clear(); _queue.clear();
} }
updateMaxSize(int size){
maxSize = size;
}
V? get(K key) => _map[key]; V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key); bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length; int get length => _map.length;

View File

@@ -5,13 +5,11 @@ import 'package:flutter/material.dart';
class Measure { class Measure {
final TextScaler _textScale; final TextScaler _textScale;
final BuildContext context; final BuildContext context;
final String? _fontFamily;
Measure.of(this.context, {String? fontFamily}) Measure.of(this.context)
: _textScale = TextScaler.linear( : _textScale = TextScaler.linear(
WidgetsBinding.instance.platformDispatcher.textScaleFactor, textScaleFactor,
), );
_fontFamily = fontFamily ?? "";
Size computeTextSize( Size computeTextSize(
Text text, { Text text, {
@@ -20,9 +18,7 @@ class Measure {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan( text: TextSpan(
text: text.data, text: text.data,
style: text.style?.copyWith( style: text.style,
fontFamily: _fontFamily,
),
), ),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: _textScale, textScaler: _textScale,

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart'; import 'package:riverpod/riverpod.dart';
import 'context.dart'; import 'context.dart';
@@ -29,8 +30,14 @@ mixin PageMixin<T extends StatefulWidget> on State<T> {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions; commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton; commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onSearch = onSearch;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate; commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
commonScaffoldState?.updateSearchState(
(_) => onSearch != null
? AppBarSearchState(
onSearch: onSearch!,
)
: null,
);
}); });
} }

View File

@@ -14,6 +14,7 @@ class Navigation {
const NavigationItem( const NavigationItem(
icon: Icon(Icons.space_dashboard), icon: Icon(Icons.space_dashboard),
label: PageLabel.dashboard, label: PageLabel.dashboard,
keep: false,
fragment: DashboardFragment( fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard), key: GlobalObjectKey(PageLabel.dashboard),
), ),

View File

@@ -70,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
Duration get transitionDuration => const Duration(milliseconds: 500); Duration get transitionDuration => const Duration(milliseconds: 500);
@override @override
Duration get reverseTransitionDuration => const Duration(milliseconds: 250); Duration get reverseTransitionDuration => const Duration(milliseconds: 500);
} }
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
@@ -194,7 +194,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
_primaryPositionCurve = CurvedAnimation( _primaryPositionCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation, parent: widget.primaryRouteAnimation,
curve: Curves.fastEaseInToSlowEaseOut, curve: Curves.fastEaseInToSlowEaseOut,
reverseCurve: Curves.easeInOut, reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped,
); );
_secondaryPositionCurve = CurvedAnimation( _secondaryPositionCurve = CurvedAnimation(
parent: widget.secondaryRouteAnimation, parent: widget.secondaryRouteAnimation,
@@ -218,9 +218,8 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
begin: const _CommonEdgeShadowDecoration(), begin: const _CommonEdgeShadowDecoration(),
end: _CommonEdgeShadowDecoration( end: _CommonEdgeShadowDecoration(
<Color>[ <Color>[
widget.context.colorScheme.inverseSurface.withOpacity( widget.context.colorScheme.inverseSurface
0.06, .withValues(alpha: 0.02),
),
Colors.transparent, Colors.transparent,
], ],
), ),
@@ -274,7 +273,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
return; return;
} }
final double shadowWidth = 0.03 * configuration.size!.width; final double shadowWidth = 1 * configuration.size!.width;
final double shadowHeight = configuration.size!.height; final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1); final double bandWidth = shadowWidth / (colors.length - 1);

View File

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

View File

@@ -22,19 +22,19 @@ class Render {
} }
pause() { pause() {
debouncer.call( throttler.call(
DebounceTag.renderPause, DebounceTag.renderPause,
_pause, _pause,
duration: Duration(seconds: 15), duration: Duration(seconds: 5),
); );
} }
resume() { resume() {
debouncer.cancel(DebounceTag.renderPause); throttler.cancel(DebounceTag.renderPause);
_resume(); _resume();
} }
void _pause() { void _pause() async {
if (_isPaused) return; if (_isPaused) return;
_isPaused = true; _isPaused = true;
_beginFrame = _dispatcher.onBeginFrame; _beginFrame = _dispatcher.onBeginFrame;
@@ -54,4 +54,4 @@ class Render {
} }
} }
final render = system.isDesktop ? Render() : null; final Render? render = system.isDesktop ? Render() : null;

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -10,6 +11,7 @@ import 'package:flutter/cupertino.dart';
class Request { class Request {
late final Dio _dio; late final Dio _dio;
late final Dio _clashDio;
String? userAgent; String? userAgent;
Request() { Request() {
@@ -20,22 +22,24 @@ class Request {
}, },
), ),
); );
_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 { Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio final response = await _clashDio.get(
.get( url,
url, options: Options(
options: Options( responseType: ResponseType.bytes,
headers: { ),
"User-Agent": globalState.ua, );
},
responseType: ResponseType.bytes,
),
)
.timeout(
httpTimeoutDuration * 6,
);
return response; return response;
} }
@@ -64,7 +68,7 @@ class Request {
final remoteVersion = data['tag_name']; final remoteVersion = data['tag_name'];
final version = globalState.packageInfo.version; final version = globalState.packageInfo.version;
final hasUpdate = final hasUpdate =
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
if (!hasUpdate) return null; if (!hasUpdate) return null;
return data; return data;
} }
@@ -79,13 +83,19 @@ class Request {
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async { Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) { for (final source in _ipInfoSources.entries) {
try { try {
final response = await _dio.get<Map<String, dynamic>>( final response = await Dio()
source.key, .get<Map<String, dynamic>>(
cancelToken: cancelToken, source.key,
options: Options( cancelToken: cancelToken,
responseType: ResponseType.json, options: Options(
), responseType: ResponseType.json,
); ),
)
.timeout(
Duration(
seconds: 30,
),
);
if (response.statusCode != 200 || response.data == null) { if (response.statusCode != 200 || response.data == null) {
continue; continue;
} }
@@ -109,9 +119,6 @@ class Request {
.get( .get(
"http://$localhost:$helperPort/ping", "http://$localhost:$helperPort/ping",
options: Options( options: Options(
headers: {
"User-Agent": browserUa,
},
responseType: ResponseType.plain, responseType: ResponseType.plain,
), ),
) )

View File

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

View File

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

39
lib/common/theme.dart Normal file
View File

@@ -0,0 +1,39 @@
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

@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/utils.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -10,7 +11,6 @@ import 'package:tray_manager/tray_manager.dart';
import 'app_localizations.dart'; import 'app_localizations.dart';
import 'constant.dart'; import 'constant.dart';
import 'other.dart';
import 'window.dart'; import 'window.dart';
class Tray { class Tray {
@@ -25,7 +25,7 @@ class Tray {
await trayManager.destroy(); await trayManager.destroy();
} }
await trayManager.setIcon( await trayManager.setIcon(
other.getTrayIconPath( utils.getTrayIconPath(
brightness: brightness ?? brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness, WidgetsBinding.instance.platformDispatcher.platformBrightness,
), ),

View File

@@ -7,7 +7,7 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart'; import 'package:lpinyin/lpinyin.dart';
class Other { class Utils {
Color? getDelayColor(int? delay) { Color? getDelayColor(int? delay) {
if (delay == null) return null; if (delay == null) return null;
if (delay < 0) return Colors.red; if (delay < 0) return Colors.red;
@@ -233,6 +233,63 @@ class Other {
return max((viewWidth / 350).floor(), 1); return max((viewWidth / 350).floor(), 1);
} }
final _indexPrimary = [
50,
100,
200,
300,
400,
500,
600,
700,
800,
850,
900,
];
_createPrimarySwatch(Color color) {
final Map<int, Color> swatch = <int, Color>{};
final int a = color.alpha8bit;
final int r = color.red8bit;
final int g = color.green8bit;
final int b = color.blue8bit;
for (final int strength in _indexPrimary) {
final double ds = 0.5 - strength / 1000;
swatch[strength] = Color.fromARGB(
a,
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
);
}
swatch[50] = swatch[50]!.lighten(18);
swatch[100] = swatch[100]!.lighten(16);
swatch[200] = swatch[200]!.lighten(14);
swatch[300] = swatch[300]!.lighten(10);
swatch[400] = swatch[400]!.lighten(6);
swatch[700] = swatch[700]!.darken(2);
swatch[800] = swatch[800]!.darken(3);
swatch[900] = swatch[900]!.darken(4);
return MaterialColor(color.value32bit, swatch);
}
List<Color> getMaterialColorShades(Color color) {
final swatch = _createPrimarySwatch(color);
return <Color>[
if (swatch[50] != null) swatch[50]!,
if (swatch[100] != null) swatch[100]!,
if (swatch[200] != null) swatch[200]!,
if (swatch[300] != null) swatch[300]!,
if (swatch[400] != null) swatch[400]!,
if (swatch[500] != null) swatch[500]!,
if (swatch[600] != null) swatch[600]!,
if (swatch[700] != null) swatch[700]!,
if (swatch[800] != null) swatch[800]!,
if (swatch[850] != null) swatch[850]!,
if (swatch[900] != null) swatch[900]!,
];
}
String getBackupFileName() { String getBackupFileName() {
return "${appName}_backup_${DateTime.now().show}.zip"; return "${appName}_backup_${DateTime.now().show}.zip";
} }
@@ -241,11 +298,6 @@ class Other {
return "${appName}_${DateTime.now().show}.log"; return "${appName}_${DateTime.now().show}.log";
} }
Size getScreenSize() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
Future<String?> getLocalIpAddress() async { Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list( List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false, includeLoopback: false,
@@ -273,4 +325,4 @@ class Other {
} }
} }
final other = Other(); final utils = Utils();

View File

@@ -21,12 +21,12 @@ class Window {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions( WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height), size: Size(props.width, props.height),
minimumSize: const Size(380, 500), minimumSize: const Size(380, 400),
); );
if (!Platform.isMacOS || version > 10) { if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden); await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
} }
if(!Platform.isMacOS){ if (!Platform.isMacOS) {
final left = props.left ?? 0; final left = props.left ?? 0;
final top = props.top ?? 0; final top = props.top ?? 0;
final right = left + props.width; final right = left + props.width;
@@ -36,7 +36,7 @@ class Window {
} else { } else {
final displays = await screenRetriever.getAllDisplays(); final displays = await screenRetriever.getAllDisplays();
final isPositionValid = displays.any( final isPositionValid = displays.any(
(display) { (display) {
final displayBounds = Rect.fromLTWH( final displayBounds = Rect.fromLTWH(
display.visiblePosition!.dx, display.visiblePosition!.dx,
display.visiblePosition!.dy, display.visiblePosition!.dy,
@@ -69,8 +69,10 @@ class Window {
await windowManager.setSkipTaskbar(false); await windowManager.setSkipTaskbar(false);
} }
Future<bool> isVisible() async { Future<bool> get isVisible async {
return await windowManager.isVisible(); final value = await windowManager.isVisible();
commonPrint.log("window visible check: $value");
return value;
} }
close() async { close() async {

View File

@@ -10,12 +10,14 @@ import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart'; import 'common/common.dart';
import 'fragments/profiles/override_profile.dart';
import 'models/models.dart'; import 'models/models.dart';
class AppController { class AppController {
@@ -28,7 +30,10 @@ class AppController {
AppController(this.context, WidgetRef ref) : _ref = ref; AppController(this.context, WidgetRef ref) : _ref = ref;
updateClashConfigDebounce() { updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig); debouncer.call(DebounceTag.updateClashConfig, () async {
final isPatch = globalState.appState.needApply ? false : true;
await updateClashConfig(isPatch);
});
} }
updateGroupsDebounce() { updateGroupsDebounce() {
@@ -41,10 +46,12 @@ class AppController {
}); });
} }
applyProfileDebounce() { applyProfileDebounce({
debouncer.call(DebounceTag.addCheckIpNum, () { bool silence = false,
applyProfile(); }) {
}); debouncer.call(DebounceTag.applyProfile, (silence) {
applyProfile(silence: silence);
}, args: [silence]);
} }
savePreferencesDebounce() { savePreferencesDebounce() {
@@ -64,7 +71,7 @@ class AppController {
restartCore() async { restartCore() async {
await clashService?.reStart(); await clashService?.reStart();
await initCore(); await _initCore();
if (_ref.read(runTimeProvider.notifier).isStart) { if (_ref.read(runTimeProvider.notifier).isStart) {
await globalState.handleStart(); await globalState.handleStart();
@@ -94,7 +101,6 @@ class AppController {
_ref.read(trafficsProvider.notifier).clear(); _ref.read(trafficsProvider.notifier).clear();
_ref.read(totalTrafficProvider.notifier).value = Traffic(); _ref.read(totalTrafficProvider.notifier).value = Traffic();
_ref.read(runTimeProvider.notifier).value = null; _ref.read(runTimeProvider.notifier).value = null;
// tray.updateTrayTitle(null);
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
} }
@@ -147,7 +153,7 @@ class AppController {
updateLocalIp() async { updateLocalIp() async {
_ref.read(localIpProvider.notifier).value = null; _ref.read(localIpProvider.notifier).value = null;
await Future.delayed(commonDuration); await Future.delayed(commonDuration);
_ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress(); _ref.read(localIpProvider.notifier).value = await utils.getLocalIpAddress();
} }
Future<void> updateProfile(Profile profile) async { Future<void> updateProfile(Profile profile) async {
@@ -156,18 +162,18 @@ class AppController {
.read(profilesProvider.notifier) .read(profilesProvider.notifier)
.setProfile(newProfile.copyWith(isUpdating: false)); .setProfile(newProfile.copyWith(isUpdating: false));
if (profile.id == _ref.read(currentProfileIdProvider)) { if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(); applyProfileDebounce(silence: true);
} }
} }
_setProfile(Profile profile) { setProfile(Profile profile) {
_ref.read(profilesProvider.notifier).setProfile(profile); _ref.read(profilesProvider.notifier).setProfile(profile);
} }
setProfile(Profile profile) { setProfileAndAutoApply(Profile profile) {
_setProfile(profile); _ref.read(profilesProvider.notifier).setProfile(profile);
if (profile.id == _ref.read(currentProfileIdProvider)) { if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(); applyProfileDebounce(silence: true);
} }
} }
@@ -219,8 +225,8 @@ class AppController {
return currentGroupName; return currentGroupName;
} }
getRealProxyName(proxyName) { ProxyCardState getProxyCardState(proxyName) {
return _ref.read(getRealTestUrlProvider(proxyName)); return _ref.read(getProxyCardStateProvider(proxyName));
} }
getSelectedProxyName(groupName) { getSelectedProxyName(groupName) {
@@ -232,12 +238,13 @@ class AppController {
if (profile == null || profile.currentGroupName == groupName) { if (profile == null || profile.currentGroupName == groupName) {
return; return;
} }
_setProfile( setProfile(
profile.copyWith(currentGroupName: groupName), profile.copyWith(currentGroupName: groupName),
); );
} }
Future<void> updateClashConfig([bool? isPatch]) async { Future<void> updateClashConfig([bool? isPatch]) async {
commonPrint.log("update clash patch: ${isPatch ?? false}");
final commonScaffoldState = globalState.homeScaffoldKey.currentState; final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return; if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async { await commonScaffoldState?.loadingRun(() async {
@@ -276,6 +283,9 @@ class AppController {
final res = await clashCore.updateConfig( final res = await clashCore.updateConfig(
globalState.getUpdateConfigParams(isPatch), globalState.getUpdateConfigParams(isPatch),
); );
if (isPatch == false) {
_ref.read(needApplyProvider.notifier).value = false;
}
if (res.isNotEmpty) throw res; if (res.isNotEmpty) throw res;
lastTunEnable = enableTun; lastTunEnable = enableTun;
lastProfileModified = await profile?.profileLastModified; lastProfileModified = await profile?.profileLastModified;
@@ -410,10 +420,13 @@ class AppController {
Map<String, dynamic>? data, Map<String, dynamic>? data,
bool handleError = false, bool handleError = false,
}) async { }) async {
if (globalState.isPre) {
return;
}
if (data != null) { if (data != null) {
final tagName = data['tag_name']; final tagName = data['tag_name'];
final body = data['body']; final body = data['body'];
final submits = other.parseReleaseBody(body); final submits = utils.parseReleaseBody(body);
final textTheme = context.textTheme; final textTheme = context.textTheme;
final res = await globalState.showMessage( final res = await globalState.showMessage(
title: appLocalizations.discoverNewVersion, title: appLocalizations.discoverNewVersion,
@@ -468,7 +481,7 @@ class AppController {
await handleExit(); await handleExit();
} }
Future<void> initCore() async { Future<void> _initCore() async {
final isInit = await clashCore.isInit; final isInit = await clashCore.isInit;
if (!isInit) { if (!isInit) {
await clashCore.setState( await clashCore.setState(
@@ -482,7 +495,7 @@ class AppController {
init() async { init() async {
await _handlePreference(); await _handlePreference();
await _handlerDisclaimer(); await _handlerDisclaimer();
await initCore(); await _initCore();
await _initStatus(); await _initStatus();
updateTray(true); updateTray(true);
autoLaunch?.updateStatus( autoLaunch?.updateStatus(
@@ -516,37 +529,12 @@ class AppController {
_ref.read(delayDataSourceProvider.notifier).setDelay(delay); _ref.read(delayDataSourceProvider.notifier).setDelay(delay);
} }
toPage( toPage(PageLabel pageLabel) {
int index, { _ref.read(currentPageLabelProvider.notifier).value = pageLabel;
bool hasAnimate = false,
}) {
final navigations = _ref.read(currentNavigationsStateProvider).value;
if (index > navigations.length - 1) {
return;
}
_ref.read(currentPageLabelProvider.notifier).value =
navigations[index].label;
final isAnimateToPage = _ref.read(appSettingProvider).isAnimateToPage;
final isMobile =
_ref.read(viewWidthProvider.notifier).viewMode == ViewMode.mobile;
if (isAnimateToPage && isMobile || hasAnimate) {
globalState.pageController?.animateToPage(
index,
duration: kTabScrollDuration,
curve: Curves.easeOut,
);
} else {
globalState.pageController?.jumpToPage(index);
}
} }
toProfiles() { toProfiles() {
final index = _ref.read(currentNavigationsStateProvider).value.indexWhere( toPage(PageLabel.profiles);
(element) => element.label == PageLabel.profiles,
);
if (index != -1) {
toPage(index);
}
} }
initLink() { initLink() {
@@ -583,17 +571,8 @@ class AppController {
Future<bool> showDisclaimer() async { Future<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>( return await globalState.showCommonDialog<bool>(
dismissible: false, dismissible: false,
child: AlertDialog( child: CommonDialog(
title: Text(appLocalizations.disclaimer), title: appLocalizations.disclaimer,
content: Container(
width: dialogCommonWidth,
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: SelectableText(
appLocalizations.disclaimerDesc,
),
),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -611,6 +590,9 @@ class AppController {
child: Text(appLocalizations.agree), child: Text(appLocalizations.agree),
) )
], ],
child: SelectableText(
appLocalizations.disclaimerDesc,
),
), ),
) ?? ) ??
false; false;
@@ -676,9 +658,9 @@ class AppController {
addProfileFormURL(url); addProfileFormURL(url);
} }
updateViewWidth(double width) { updateViewSize(Size size) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_ref.read(viewWidthProvider.notifier).value = width; _ref.read(viewSizeProvider.notifier).value = size;
}); });
} }
@@ -689,9 +671,9 @@ class AppController {
List<Proxy> _sortOfName(List<Proxy> proxies) { List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies) return List.of(proxies)
..sort( ..sort(
(a, b) => other.sortByChar( (a, b) => utils.sortByChar(
other.getPinyin(a.name), utils.getPinyin(a.name),
other.getPinyin(b.name), utils.getPinyin(b.name),
), ),
); );
} }
@@ -737,10 +719,18 @@ class AppController {
final providersPath = await appPath.getProvidersPath(profileId); final providersPath = await appPath.getProvidersPath(profileId);
return await Isolate.run(() async { return await Isolate.run(() async {
if (profilePath != null) { if (profilePath != null) {
await File(profilePath).delete(recursive: true); final profileFile = File(profilePath);
final isExists = await profileFile.exists();
if (isExists) {
profileFile.delete(recursive: true);
}
} }
if (providersPath != null) { if (providersPath != null) {
await File(providersPath).delete(recursive: true); final providersFileDir = File(providersPath);
final isExists = await providersFileDir.exists();
if (isExists) {
providersFileDir.delete(recursive: true);
}
} }
}); });
} }
@@ -754,13 +744,13 @@ class AppController {
updateSystemProxy() { updateSystemProxy() {
_ref.read(networkSettingProvider.notifier).updateState( _ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith( (state) => state.copyWith(
systemProxy: state.systemProxy, systemProxy: !state.systemProxy,
), ),
); );
} }
updateStart() { updateStart() {
updateStatus(_ref.read(runTimeProvider.notifier).isStart); updateStatus(!_ref.read(runTimeProvider.notifier).isStart);
} }
updateCurrentSelectedMap(String groupName, String proxyName) { updateCurrentSelectedMap(String groupName, String proxyName) {
@@ -809,7 +799,7 @@ class AppController {
} }
updateVisible() async { updateVisible() async {
final visible = await window?.isVisible(); final visible = await window?.isVisible;
if (visible != null && !visible) { if (visible != null && !visible) {
window?.show(); window?.show();
} else { } else {
@@ -832,6 +822,38 @@ class AppController {
); );
} }
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) {
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;
},
);
}
Future<bool> exportLogs() async { Future<bool> exportLogs() async {
final logsRaw = _ref.read(logsProvider).list.map( final logsRaw = _ref.read(logsProvider).list.map(
(item) => item.toString(), (item) => item.toString(),
@@ -841,7 +863,7 @@ class AppController {
return utf8.encode(logsRawString); return utf8.encode(logsRawString);
}); });
return await picker.saveFile( return await picker.saveFile(
other.logFile, utils.logFile,
Uint8List.fromList(data), Uint8List.fromList(data),
) != ) !=
null; null;

View File

@@ -219,14 +219,13 @@ enum ProxiesIconStyle {
} }
enum FontFamily { enum FontFamily {
system(),
miSans("MiSans"),
twEmoji("Twemoji"), twEmoji("Twemoji"),
jetBrainsMono("JetBrainsMono"),
icon("Icons"); icon("Icons");
final String? value; final String value;
const FontFamily([this.value]); const FontFamily(this.value);
} }
enum RouteMode { enum RouteMode {
@@ -386,3 +385,65 @@ enum PageLabel {
resources, resources,
connections, 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

@@ -150,9 +150,17 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
return IconButton( return IconButton(
onPressed: () async { onPressed: () async {
final res = await showSheet<int>( final res = await showSheet<int>(
title: appLocalizations.proxiesSetting,
context: context, context: context,
body: AccessControlPanel(), props: SheetProps(
isScrollControlled: true,
),
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: AccessControlPanel(),
title: appLocalizations.proxiesSetting,
);
},
); );
if (res == 1) { if (res == 1) {
_intelligentSelected(); _intelligentSelected();
@@ -763,17 +771,19 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 32), child: Padding(
child: Column( padding: const EdgeInsets.only(bottom: 32),
mainAxisSize: MainAxisSize.min, child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
..._buildModeSetting(), children: [
..._buildSortSetting(), ..._buildModeSetting(),
..._buildSourceSetting(), ..._buildSortSetting(),
..._buildActionSetting(), ..._buildSourceSetting(),
], ..._buildActionSetting(),
],
),
), ),
); );
} }

View File

@@ -276,8 +276,8 @@ class ApplicationSettingFragment extends StatelessWidget {
AutoRunItem(), AutoRunItem(),
if (Platform.isAndroid) ...[ if (Platform.isAndroid) ...[
HiddenItem(), HiddenItem(),
AnimateTabItem(),
], ],
AnimateTabItem(),
OpenLogsItem(), OpenLogsItem(),
CloseConnectionsItem(), CloseConnectionsItem(),
UsageItem(), UsageItem(),

View File

@@ -6,6 +6,7 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.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/fade_box.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart'; import 'package:fl_clash/widgets/text.dart';
@@ -74,7 +75,7 @@ class BackupAndRecovery extends ConsumerWidget {
() async { () async {
final backupData = await globalState.appController.backupData(); final backupData = await globalState.appController.backupData();
final value = await picker.saveFile( final value = await picker.saveFile(
other.getBackupFileName(), utils.getBackupFileName(),
Uint8List.fromList(backupData), Uint8List.fromList(backupData),
); );
if (value == null) return false; if (value == null) return false;
@@ -174,7 +175,7 @@ class BackupAndRecovery extends ConsumerWidget {
future: client!.pingCompleter.future, future: client!.pingCompleter.future,
builder: (_, snapshot) { builder: (_, snapshot) {
return Center( return Center(
child: FadeBox( child: FadeThroughBox(
child: snapshot.connectionState == child: snapshot.connectionState ==
ConnectionState.waiting ConnectionState.waiting
? const SizedBox( ? const SizedBox(
@@ -275,30 +276,27 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return CommonDialog(
title: Text(appLocalizations.recovery), title: appLocalizations.recovery,
contentPadding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
vertical: 16, vertical: 16,
), ),
content: SizedBox( child: Wrap(
width: 250, children: [
child: Wrap( ListItem(
children: [ onTap: () {
ListItem( _handleOnTab(RecoveryOption.onlyProfiles);
onTap: () { },
_handleOnTab(RecoveryOption.onlyProfiles); title: Text(appLocalizations.recoveryProfiles),
}, ),
title: Text(appLocalizations.recoveryProfiles), ListItem(
), onTap: () {
ListItem( _handleOnTab(RecoveryOption.all);
onTap: () { },
_handleOnTab(RecoveryOption.all); title: Text(appLocalizations.recoveryAll),
}, )
title: Text(appLocalizations.recoveryAll), ],
)
],
),
), ),
); );
} }
@@ -351,78 +349,8 @@ class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return CommonDialog(
title: Text(appLocalizations.webDAVConfiguration), title: 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: [ actions: [
if (widget.dav != null) if (widget.dav != null)
TextButton( TextButton(
@@ -434,6 +362,73 @@ class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
child: Text(appLocalizations.save), 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,8 +2,13 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/config/dns.dart'; import 'package:fl_clash/fragments/config/dns.dart';
import 'package:fl_clash/fragments/config/general.dart'; import 'package:fl_clash/fragments/config/general.dart';
import 'package:fl_clash/fragments/config/network.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:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../state.dart';
class ConfigFragment extends StatefulWidget { class ConfigFragment extends StatefulWidget {
const ConfigFragment({super.key}); const ConfigFragment({super.key});
@@ -16,18 +21,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> items = [ 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( ListItem.open(
title: Text(appLocalizations.general), title: Text(appLocalizations.general),
subtitle: Text(appLocalizations.generalDesc), subtitle: Text(appLocalizations.generalDesc),
@@ -37,20 +30,51 @@ class _ConfigFragmentState extends State<ConfigFragment> {
widget: generateListView( widget: generateListView(
generalItems, generalItems,
), ),
isBlur: false, blur: false,
extendPageWidth: 360, ),
),
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(),
), ),
), ),
ListItem.open( ListItem.open(
title: const Text("DNS"), title: const Text("DNS"),
subtitle: Text(appLocalizations.dnsDesc), subtitle: Text(appLocalizations.dnsDesc),
leading: const Icon(Icons.dns), leading: const Icon(Icons.dns),
delegate: const OpenDelegate( delegate: OpenDelegate(
title: "DNS", title: "DNS",
widget: DnsListView(), action: Consumer(builder: (_, ref, __) {
isScaffold: true, return IconButton(
isBlur: false, onPressed: () async {
extendPageWidth: 360, 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,
), ),
) )
]; ];

View File

@@ -1,8 +1,6 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.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/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -48,6 +46,32 @@ class StatusItem extends ConsumerWidget {
} }
} }
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 ConsumerWidget {
const PreferH3Item({super.key}); const PreferH3Item({super.key});
@@ -179,7 +203,7 @@ class FakeIpFilterItem extends StatelessWidget {
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.fakeipFilter), title: Text(appLocalizations.fakeipFilter),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: appLocalizations.fakeipFilter, title: appLocalizations.fakeipFilter,
widget: Consumer( widget: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
@@ -187,7 +211,7 @@ class FakeIpFilterItem extends StatelessWidget {
patchClashConfigProvider patchClashConfigProvider
.select((state) => state.dns.fakeIpFilter), .select((state) => state.dns.fakeIpFilter),
); );
return ListPage( return ListInputPage(
title: appLocalizations.fakeipFilter, title: appLocalizations.fakeipFilter,
items: fakeIpFilter, items: fakeIpFilter,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -201,7 +225,6 @@ class FakeIpFilterItem extends StatelessWidget {
); );
}, },
), ),
extendPageWidth: 360,
), ),
); );
} }
@@ -216,14 +239,14 @@ class DefaultNameserverItem extends StatelessWidget {
title: Text(appLocalizations.defaultNameserver), title: Text(appLocalizations.defaultNameserver),
subtitle: Text(appLocalizations.defaultNameserverDesc), subtitle: Text(appLocalizations.defaultNameserverDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: appLocalizations.defaultNameserver, title: appLocalizations.defaultNameserver,
widget: Consumer(builder: (_, ref, __) { widget: Consumer(builder: (_, ref, __) {
final defaultNameserver = ref.watch( final defaultNameserver = ref.watch(
patchClashConfigProvider patchClashConfigProvider
.select((state) => state.dns.defaultNameserver), .select((state) => state.dns.defaultNameserver),
); );
return ListPage( return ListInputPage(
title: appLocalizations.defaultNameserver, title: appLocalizations.defaultNameserver,
items: defaultNameserver, items: defaultNameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -236,7 +259,6 @@ class DefaultNameserverItem extends StatelessWidget {
}, },
); );
}), }),
extendPageWidth: 360,
), ),
); );
} }
@@ -252,13 +274,13 @@ class NameserverItem extends StatelessWidget {
subtitle: Text(appLocalizations.nameserverDesc), subtitle: Text(appLocalizations.nameserverDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
title: appLocalizations.nameserver, title: appLocalizations.nameserver,
isBlur: false, blur: false,
widget: Consumer(builder: (_, ref, __) { widget: Consumer(builder: (_, ref, __) {
final nameserver = ref.watch( final nameserver = ref.watch(
patchClashConfigProvider.select((state) => state.dns.nameserver), patchClashConfigProvider.select((state) => state.dns.nameserver),
); );
return ListPage( return ListInputPage(
title: "域名服务器", title: appLocalizations.nameserver,
items: nameserver, items: nameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items) { onChange: (items) {
@@ -270,7 +292,6 @@ class NameserverItem extends StatelessWidget {
}, },
); );
}), }),
extendPageWidth: 360,
), ),
); );
} }
@@ -331,28 +352,27 @@ class NameserverPolicyItem extends StatelessWidget {
title: Text(appLocalizations.nameserverPolicy), title: Text(appLocalizations.nameserverPolicy),
subtitle: Text(appLocalizations.nameserverPolicyDesc), subtitle: Text(appLocalizations.nameserverPolicyDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: appLocalizations.nameserverPolicy, title: appLocalizations.nameserverPolicy,
widget: Consumer(builder: (_, ref, __) { widget: Consumer(builder: (_, ref, __) {
final nameserverPolicy = ref.watch( final nameserverPolicy = ref.watch(
patchClashConfigProvider patchClashConfigProvider
.select((state) => state.dns.nameserverPolicy), .select((state) => state.dns.nameserverPolicy),
); );
return ListPage( return MapInputPage(
title: appLocalizations.nameserverPolicy, title: appLocalizations.nameserverPolicy,
items: nameserverPolicy.entries, map: nameserverPolicy,
titleBuilder: (item) => Text(item.key), titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value), subtitleBuilder: (item) => Text(item.value),
onChange: (items) { onChange: (value) {
ref.read(patchClashConfigProvider.notifier).updateState( ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns( (state) => state.copyWith.dns(
nameserverPolicy: Map.fromEntries(items), nameserverPolicy: value,
), ),
); );
}, },
); );
}), }),
extendPageWidth: 360,
), ),
); );
} }
@@ -367,7 +387,7 @@ class ProxyServerNameserverItem extends StatelessWidget {
title: Text(appLocalizations.proxyNameserver), title: Text(appLocalizations.proxyNameserver),
subtitle: Text(appLocalizations.proxyNameserverDesc), subtitle: Text(appLocalizations.proxyNameserverDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: appLocalizations.proxyNameserver, title: appLocalizations.proxyNameserver,
widget: Consumer( widget: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
@@ -375,7 +395,7 @@ class ProxyServerNameserverItem extends StatelessWidget {
patchClashConfigProvider patchClashConfigProvider
.select((state) => state.dns.proxyServerNameserver), .select((state) => state.dns.proxyServerNameserver),
); );
return ListPage( return ListInputPage(
title: appLocalizations.proxyNameserver, title: appLocalizations.proxyNameserver,
items: proxyServerNameserver, items: proxyServerNameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -389,7 +409,6 @@ class ProxyServerNameserverItem extends StatelessWidget {
); );
}, },
), ),
extendPageWidth: 360,
), ),
); );
} }
@@ -404,13 +423,13 @@ class FallbackItem extends StatelessWidget {
title: Text(appLocalizations.fallback), title: Text(appLocalizations.fallback),
subtitle: Text(appLocalizations.fallbackDesc), subtitle: Text(appLocalizations.fallbackDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: appLocalizations.fallback, title: appLocalizations.fallback,
widget: Consumer(builder: (_, ref, __) { widget: Consumer(builder: (_, ref, __) {
final fallback = ref.watch( final fallback = ref.watch(
patchClashConfigProvider.select((state) => state.dns.fallback), patchClashConfigProvider.select((state) => state.dns.fallback),
); );
return ListPage( return ListInputPage(
title: appLocalizations.fallback, title: appLocalizations.fallback,
items: fallback, items: fallback,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -423,7 +442,6 @@ class FallbackItem extends StatelessWidget {
}, },
); );
}), }),
extendPageWidth: 360,
), ),
); );
} }
@@ -492,14 +510,14 @@ class GeositeItem extends StatelessWidget {
return ListItem.open( return ListItem.open(
title: const Text("Geosite"), title: const Text("Geosite"),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: "Geosite", title: "Geosite",
widget: Consumer(builder: (_, ref, __) { widget: Consumer(builder: (_, ref, __) {
final geosite = ref.watch( final geosite = ref.watch(
patchClashConfigProvider patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.geosite), .select((state) => state.dns.fallbackFilter.geosite),
); );
return ListPage( return ListInputPage(
title: "Geosite", title: "Geosite",
items: geosite, items: geosite,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -512,7 +530,6 @@ class GeositeItem extends StatelessWidget {
}, },
); );
}), }),
extendPageWidth: 360,
), ),
); );
} }
@@ -526,14 +543,14 @@ class IpcidrItem extends StatelessWidget {
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.ipcidr), title: Text(appLocalizations.ipcidr),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: appLocalizations.ipcidr, title: appLocalizations.ipcidr,
widget: Consumer(builder: (_, ref, ___) { widget: Consumer(builder: (_, ref, ___) {
final ipcidr = ref.watch( final ipcidr = ref.watch(
patchClashConfigProvider patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.ipcidr), .select((state) => state.dns.fallbackFilter.ipcidr),
); );
return ListPage( return ListInputPage(
title: appLocalizations.ipcidr, title: appLocalizations.ipcidr,
items: ipcidr, items: ipcidr,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -546,7 +563,6 @@ class IpcidrItem extends StatelessWidget {
}, },
); );
}), }),
extendPageWidth: 360,
), ),
); );
} }
@@ -560,14 +576,14 @@ class DomainItem extends StatelessWidget {
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.domain), title: Text(appLocalizations.domain),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: appLocalizations.domain, title: appLocalizations.domain,
widget: Consumer(builder: (_, ref, __) { widget: Consumer(builder: (_, ref, __) {
final domain = ref.watch( final domain = ref.watch(
patchClashConfigProvider patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.domain), .select((state) => state.dns.fallbackFilter.domain),
); );
return ListPage( return ListInputPage(
title: appLocalizations.domain, title: appLocalizations.domain,
items: domain, items: domain,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -580,7 +596,6 @@ class DomainItem extends StatelessWidget {
}, },
); );
}), }),
extendPageWidth: 360,
), ),
); );
} }
@@ -596,6 +611,7 @@ class DnsOptions extends StatelessWidget {
title: appLocalizations.options, title: appLocalizations.options,
items: [ items: [
const StatusItem(), const StatusItem(),
const ListenItem(),
const UseHostsItem(), const UseHostsItem(),
const UseSystemHostsItem(), const UseSystemHostsItem(),
const IPv6Item(), const IPv6Item(),
@@ -644,39 +660,8 @@ const dnsItems = <Widget>[
class DnsListView extends ConsumerWidget { class DnsListView extends ConsumerWidget {
const DnsListView({super.key}); const DnsListView({super.key});
_initActions(BuildContext context, WidgetRef ref) {
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;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
dns: defaultDns,
),
);
},
tooltip: appLocalizations.reset,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
_initActions(context, ref);
return generateListView( return generateListView(
dnsItems, dnsItems,
); );

View File

@@ -199,28 +199,27 @@ class HostsItem extends StatelessWidget {
title: const Text("Hosts"), title: const Text("Hosts"),
subtitle: Text(appLocalizations.hostsDesc), subtitle: Text(appLocalizations.hostsDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
title: "Hosts", title: "Hosts",
widget: Consumer( widget: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final hosts = ref final hosts = ref
.watch(patchClashConfigProvider.select((state) => state.hosts)); .watch(patchClashConfigProvider.select((state) => state.hosts));
return ListPage( return MapInputPage(
title: "Hosts", title: "Hosts",
items: hosts.entries, map: hosts,
titleBuilder: (item) => Text(item.key), titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value), subtitleBuilder: (item) => Text(item.value),
onChange: (items) { onChange: (value) {
ref.read(patchClashConfigProvider.notifier).updateState( ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith( (state) => state.copyWith(
hosts: Map.fromEntries(items), hosts: value,
), ),
); );
}, },
); );
}, },
), ),
extendPageWidth: 360,
), ),
); );
} }

View File

@@ -224,15 +224,14 @@ class BypassDomainItem extends StatelessWidget {
title: Text(appLocalizations.bypassDomain), title: Text(appLocalizations.bypassDomain),
subtitle: Text(appLocalizations.bypassDomainDesc), subtitle: Text(appLocalizations.bypassDomainDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
isScaffold: true,
title: appLocalizations.bypassDomain, title: appLocalizations.bypassDomain,
widget: Consumer( widget: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
_initActions(context, ref); _initActions(context, ref);
final bypassDomain = ref.watch( final bypassDomain = ref.watch(
networkSettingProvider.select((state) => state.bypassDomain)); networkSettingProvider.select((state) => state.bypassDomain));
return ListPage( return ListInputPage(
title: appLocalizations.bypassDomain, title: appLocalizations.bypassDomain,
items: bypassDomain, items: bypassDomain,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -246,7 +245,6 @@ class BypassDomainItem extends StatelessWidget {
); );
}, },
), ),
extendPageWidth: 360,
), ),
); );
} }
@@ -298,14 +296,14 @@ class RouteAddressItem extends ConsumerWidget {
title: Text(appLocalizations.routeAddress), title: Text(appLocalizations.routeAddress),
subtitle: Text(appLocalizations.routeAddressDesc), subtitle: Text(appLocalizations.routeAddressDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, blur: false,
isScaffold: true, maxWidth: 360,
title: appLocalizations.routeAddress, title: appLocalizations.routeAddress,
widget: Consumer( widget: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final routeAddress = ref.watch(patchClashConfigProvider final routeAddress = ref.watch(patchClashConfigProvider
.select((state) => state.tun.routeAddress)); .select((state) => state.tun.routeAddress));
return ListPage( return ListInputPage(
title: appLocalizations.routeAddress, title: appLocalizations.routeAddress,
items: routeAddress, items: routeAddress,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
@@ -319,7 +317,6 @@ class RouteAddressItem extends ConsumerWidget {
); );
}, },
), ),
extendPageWidth: 360,
), ),
); );
} }

View File

@@ -125,7 +125,7 @@ class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
return ConnectionItem( return ConnectionItem(
key: Key(connection.id), key: Key(connection.id),
connection: connection, connection: connection,
onClick: (value) { onClickKeyword: (value) {
context.commonScaffoldState?.addKeyword(value); context.commonScaffoldState?.addKeyword(value);
}, },
trailing: IconButton( trailing: IconButton(

View File

@@ -9,40 +9,15 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class FindProcessBuilder extends StatelessWidget { class ConnectionItem extends ConsumerWidget {
final Widget Function(bool value) builder;
const FindProcessBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Consumer(
builder: (_, ref, __) {
final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
),
);
return builder(value);
},
);
}
}
class ConnectionItem extends StatelessWidget {
final Connection connection; final Connection connection;
final Function(String)? onClick; final Function(String)? onClickKeyword;
final Widget? trailing; final Widget? trailing;
const ConnectionItem({ const ConnectionItem({
super.key, super.key,
required this.connection, required this.connection,
this.onClick, this.onClickKeyword,
this.trailing, this.trailing,
}); });
@@ -59,7 +34,14 @@ class ConnectionItem extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
),
);
final title = Text( final title = Text(
connection.desc, connection.desc,
style: context.textTheme.bodyLarge, style: context.textTheme.bodyLarge,
@@ -86,70 +68,143 @@ class ConnectionItem extends StatelessWidget {
CommonChip( CommonChip(
label: chain, label: chain,
onPressed: () { onPressed: () {
if (onClick == null) return; if (onClickKeyword == null) return;
onClick!(chain); onClickKeyword!(chain);
}, },
), ),
], ],
), ),
], ],
); );
if (!Platform.isAndroid) { return CommonPopupBox(
return ListItem( targetBuilder: (open) {
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( return ListItem(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 4, vertical: 4,
), ),
tileTitleAlignment: ListTileTitleAlignment.titleHeight, tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: leading, 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, title: title,
subtitle: subTitle, subtitle: subTitle,
trailing: trailing, 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,3 +1,5 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
@@ -9,8 +11,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart'; import 'item.dart';
double _preOffset = 0;
class RequestsFragment extends ConsumerStatefulWidget { class RequestsFragment extends ConsumerStatefulWidget {
const RequestsFragment({super.key}); const RequestsFragment({super.key});
@@ -20,15 +20,12 @@ class RequestsFragment extends ConsumerStatefulWidget {
class _RequestsFragmentState extends ConsumerState<RequestsFragment> class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin { with PageMixin {
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
final _requestsStateNotifier = final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState()); ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = []; List<Connection> _requests = [];
final _cacheKey = ValueKey("requests_list");
final ScrollController _scrollController = ScrollController( late ScrollController _scrollController;
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0; double _currentMaxWidth = 0;
@@ -48,10 +45,13 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1;
_scrollController = ScrollController(
initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite,
);
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith( _requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: globalState.appState.requests.list, connections: globalState.appState.requests.list,
); );
ref.listenManual( ref.listenManual(
isCurrentPageProvider( isCurrentPageProvider(
PageLabel.requests, PageLabel.requests,
@@ -78,10 +78,6 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
} }
double _calcCacheHeight(Connection item) { double _calcCacheHeight(Connection item) {
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize( final size = globalState.measure.computeTextSize(
Text( Text(
item.desc, item.desc,
@@ -102,14 +98,13 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
final lines = (chainSize.height / baseHeight).round(); final lines = (chainSize.height / baseHeight).round();
final computerHeight = final computerHeight =
size.height + chainSize.height + 24 + 24 * (lines - 1); size.height + chainSize.height + 24 + 24 * (lines - 1);
_cacheDynamicHeightMap.put(item.id, computerHeight);
return computerHeight; return computerHeight;
} }
_handleTryClearCache(double maxWidth) { _handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) { if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth; _currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear(); _key.currentState?.clearCache();
} }
} }
@@ -118,7 +113,6 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
_requestsStateNotifier.dispose(); _requestsStateNotifier.dispose();
_scrollController.dispose(); _scrollController.dispose();
_currentMaxWidth = 0; _currentMaxWidth = 0;
_cacheDynamicHeightMap.clear();
super.dispose(); super.dispose();
} }
@@ -143,9 +137,19 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (_, constraints) { builder: (_, constraints) {
return FindProcessBuilder(builder: (value) { return Consumer(
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0)); builder: (_, ref, child) {
return ValueListenableBuilder<ConnectionsState>( final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
),
);
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return child!;
},
child: ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier, valueListenable: _requestsStateNotifier,
builder: (_, state, __) { builder: (_, state, __) {
final connections = state.list; final connections = state.list;
@@ -159,7 +163,7 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
(connection) => ConnectionItem( (connection) => ConnectionItem(
key: Key(connection.id), key: Key(connection.id),
connection: connection, connection: connection,
onClick: (value) { onClickKeyword: (value) {
context.commonScaffoldState?.addKeyword(value); context.commonScaffoldState?.addKeyword(value);
}, },
), ),
@@ -172,19 +176,19 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
.toList(); .toList();
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>( child: ScrollToEndBox(
onNotification: (details) { controller: _scrollController,
_preOffset = details.metrics.pixels; cacheKey: _cacheKey,
return false; dataSource: connections,
},
child: CommonScrollBar( child: CommonScrollBar(
controller: _scrollController, controller: _scrollController,
child: ListView.builder( child: CacheItemExtentListView(
key: _key,
reverse: true, reverse: true,
shrinkWrap: true, shrinkWrap: true,
physics: NextClampingScrollPhysics(), physics: NextClampingScrollPhysics(),
controller: _scrollController, controller: _scrollController,
itemExtentBuilder: (index, __) { itemExtentBuilder: (index) {
final widget = items[index]; final widget = items[index];
if (widget.runtimeType == Divider) { if (widget.runtimeType == Divider) {
return 0; return 0;
@@ -199,13 +203,21 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
return items[index]; return items[index];
}, },
itemCount: items.length, 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

@@ -31,7 +31,7 @@ class IntranetIP extends StatelessWidget {
child: Consumer( child: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final localIp = ref.watch(localIpProvider); final localIp = ref.watch(localIpProvider);
return FadeBox( return FadeThroughBox(
child: localIp != null child: localIp != null
? TooltipText( ? TooltipText(
text: Text( text: Text(

View File

@@ -39,7 +39,7 @@ class _MemoryInfoState extends State<MemoryInfo> {
_memoryInfoStateNotifier.value = TrafficValue( _memoryInfoStateNotifier.value = TrafficValue(
value: clashLib != null ? rss : await clashCore.getMemory() + rss, value: clashLib != null ? rss : await clashCore.getMemory() + rss,
); );
timer = Timer(Duration(seconds: 5), () async { timer = Timer(Duration(seconds: 2), () async {
_updateMemory(); _updateMemory();
}); });
}); });
@@ -47,13 +47,8 @@ class _MemoryInfoState extends State<MemoryInfo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final darkenLighter = context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.toLighter;
final darken = context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1);
return SizedBox( return SizedBox(
height: getWidgetHeight(2), height: getWidgetHeight(1),
child: CommonCard( child: CommonCard(
info: Info( info: Info(
iconData: Icons.memory, iconData: Icons.memory,
@@ -62,12 +57,12 @@ class _MemoryInfoState extends State<MemoryInfo> {
onPressed: () { onPressed: () {
clashCore.requestGc(); clashCore.requestGc();
}, },
child: ValueListenableBuilder( child: Column(
valueListenable: _memoryInfoStateNotifier, children: [
builder: (_, trafficValue, __) { ValueListenableBuilder(
return Column( valueListenable: _memoryInfoStateNotifier,
children: [ builder: (_, trafficValue, __) {
Padding( return Padding(
padding: baseInfoEdgeInsets.copyWith( padding: baseInfoEdgeInsets.copyWith(
bottom: 0, bottom: 0,
top: 12, top: 12,
@@ -76,43 +71,94 @@ class _MemoryInfoState extends State<MemoryInfo> {
children: [ children: [
Text( Text(
trafficValue.showValue, trafficValue.showValue,
style: context.textTheme.titleLarge?.toLight, style:
context.textTheme.bodyMedium?.toLight.adjustSize(1),
), ),
SizedBox( SizedBox(
width: 8, width: 8,
), ),
Text( Text(
trafficValue.showUnit, trafficValue.showUnit,
style: context.textTheme.titleLarge?.toLight, style:
context.textTheme.bodyMedium?.toLight.adjustSize(1),
) )
], ],
), ),
), );
Flexible( },
child: Stack( ),
children: [ ],
Positioned.fill(
child: WaveView(
waveAmplitude: 12.0,
waveFrequency: 0.35,
waveColor: darkenLighter,
),
),
Positioned.fill(
child: WaveView(
waveAmplitude: 12.0,
waveFrequency: 0.9,
waveColor: darken,
),
),
],
),
)
],
);
},
), ),
), ),
); );
} }
} }
// class AnimatedCounter extends StatefulWidget {
// final double value;
// final TextStyle? style;
//
// const AnimatedCounter({
// super.key,
// required this.value,
// this.style,
// });
//
// @override
// State<AnimatedCounter> createState() => _AnimatedCounterState();
// }
//
// class _AnimatedCounterState extends State<AnimatedCounter> {
// late double _previousValue;
// late double _currentValue;
//
// @override
// void initState() {
// super.initState();
// _previousValue = widget.value;
// _currentValue = widget.value;
// }
//
// @override
// void didUpdateWidget(AnimatedCounter oldWidget) {
// super.didUpdateWidget(oldWidget);
// if (oldWidget.value != widget.value) {
// // if (_previousValue == _currentValue) {
// // _previousValue = widget.value;
// // _currentValue = widget.value;
// // return;
// // }
// _currentValue = widget.value;
// }
// }
//
// @override
// void dispose() {
// super.dispose();
// }
//
// @override
// Widget build(BuildContext context) {
// return Text(
// _currentValue.fixed(decimals: 1),
// style: widget.style,
// );
// return TweenAnimationBuilder(
// tween: Tween(
// begin: _previousValue,
// end: _currentValue,
// ),
// onEnd: () {
// _previousValue = _currentValue;
// },
// duration: Duration(seconds: 6),
// curve: Curves.easeOut,
// builder: (_, value, ___) {
// return Text(
// value.fixed(decimals: 1),
// style: widget.style,
// );
// },
// );
// }
// }

View File

@@ -12,7 +12,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
final _networkDetectionState = ValueNotifier<NetworkDetectionState>( final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState( const NetworkDetectionState(
isTesting: true, isTesting: false,
isLoading: true,
ipInfo: null, ipInfo: null,
), ),
); );
@@ -28,7 +29,6 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
bool? _preIsStart; bool? _preIsStart;
Timer? _setTimeoutTimer; Timer? _setTimeoutTimer;
CancelToken? cancelToken; CancelToken? cancelToken;
Completer? checkedCompleter;
@override @override
void initState() { void initState() {
@@ -37,11 +37,14 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
_startCheck(); _startCheck();
} }
}); });
if (!_networkDetectionState.value.isTesting &&
_networkDetectionState.value.isLoading) {
_startCheck();
}
super.initState(); super.initState();
} }
_startCheck() async { _startCheck() async {
await checkedCompleter?.future;
if (cancelToken != null) { if (cancelToken != null) {
cancelToken!.cancel(); cancelToken!.cancel();
cancelToken = null; cancelToken = null;
@@ -59,10 +62,12 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
final isStart = appState.runTime != null; final isStart = appState.runTime != null;
if (_preIsStart == false && if (_preIsStart == false &&
_preIsStart == isStart && _preIsStart == isStart &&
_networkDetectionState.value.ipInfo != null) return; _networkDetectionState.value.ipInfo != null) {
return;
}
_clearSetTimeoutTimer(); _clearSetTimeoutTimer();
_networkDetectionState.value = _networkDetectionState.value.copyWith( _networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true, isLoading: true,
ipInfo: null, ipInfo: null,
); );
_preIsStart = isStart; _preIsStart = isStart;
@@ -72,16 +77,16 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
} }
cancelToken = CancelToken(); cancelToken = CancelToken();
try { try {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true,
);
final ipInfo = await request.checkIp(cancelToken: cancelToken); final ipInfo = await request.checkIp(cancelToken: cancelToken);
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false,
);
if (ipInfo != null) { if (ipInfo != null) {
checkedCompleter = Completer();
checkedCompleter?.complete(
Future.delayed(
Duration(milliseconds: 3000),
),
);
_networkDetectionState.value = _networkDetectionState.value.copyWith( _networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false, isLoading: false,
ipInfo: ipInfo, ipInfo: ipInfo,
); );
return; return;
@@ -89,14 +94,14 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
_clearSetTimeoutTimer(); _clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () { _setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
_networkDetectionState.value = _networkDetectionState.value.copyWith( _networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false, isLoading: false,
ipInfo: null, ipInfo: null,
); );
}); });
} catch (e) { } catch (e) {
if (e.toString() == "cancelled") { if (e.toString() == "cancelled") {
_networkDetectionState.value = _networkDetectionState.value.copyWith( _networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true, isLoading: true,
ipInfo: null, ipInfo: null,
); );
} }
@@ -134,7 +139,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
valueListenable: _networkDetectionState, valueListenable: _networkDetectionState,
builder: (_, state, __) { builder: (_, state, __) {
final ipInfo = state.ipInfo; final ipInfo = state.ipInfo;
final isTesting = state.isTesting; final isLoading = state.isLoading;
return CommonCard( return CommonCard(
onPressed: () {}, onPressed: () {},
child: Column( child: Column(
@@ -216,7 +221,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
), ),
child: SizedBox( child: SizedBox(
height: globalState.measure.bodyMediumHeight + 2, height: globalState.measure.bodyMediumHeight + 2,
child: FadeBox( child: FadeThroughBox(
child: ipInfo != null child: ipInfo != null
? TooltipText( ? TooltipText(
text: Text( text: Text(
@@ -227,8 +232,8 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
) )
: FadeBox( : FadeThroughBox(
child: isTesting == false && ipInfo == null child: isLoading == false && ipInfo == null
? Text( ? Text(
"timeout", "timeout",
style: context.textTheme.bodyMedium style: context.textTheme.bodyMedium

View File

@@ -41,7 +41,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = context.colorScheme.onSurfaceVariant.toLight; final color = context.colorScheme.onSurfaceVariant.opacity80;
return SizedBox( return SizedBox(
height: getWidgetHeight(2), height: getWidgetHeight(2),
child: CommonCard( child: CommonCard(

View File

@@ -38,7 +38,7 @@ class OutboundMode extends StatelessWidget {
for (final item in Mode.values) for (final item in Mode.values)
Flexible( Flexible(
child: ListItem.radio( child: ListItem.radio(
prue: true, dense: true,
horizontalTitleGap: 4, horizontalTitleGap: 4,
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 12, left: 12,

View File

@@ -16,13 +16,20 @@ class TUNButton extends StatelessWidget {
onPressed: () { onPressed: () {
showSheet( showSheet(
context: context, context: context,
body: generateListView(generateSection( builder: (_, type) {
items: [ return AdaptiveSheetScaffold(
if (system.isDesktop) const TUNItem(), type: type,
const TunStackItem(), body: generateListView(
], generateSection(
)), items: [
title: appLocalizations.tun, if (system.isDesktop) const TUNItem(),
const TunStackItem(),
],
),
),
title: appLocalizations.tun,
);
},
); );
}, },
info: Info( info: Info(
@@ -89,15 +96,20 @@ class SystemProxyButton extends StatelessWidget {
onPressed: () { onPressed: () {
showSheet( showSheet(
context: context, context: context,
body: generateListView( builder: (_, type) {
generateSection( return AdaptiveSheetScaffold(
items: [ type: type,
SystemProxyItem(), body: generateListView(
BypassDomainItem(), generateSection(
], items: [
), SystemProxyItem(),
), BypassDomainItem(),
title: appLocalizations.systemProxy, ],
),
),
title: appLocalizations.systemProxy,
);
},
); );
}, },
info: Info( info: Info(

View File

@@ -71,10 +71,10 @@ class _StartButtonState extends State<StartButton>
final textWidth = globalState.measure final textWidth = globalState.measure
.computeTextSize( .computeTextSize(
Text( Text(
other.getTimeDifference( utils.getTimeDifference(
DateTime.now(), DateTime.now(),
), ),
style: Theme.of(context).textTheme.titleMedium?.toSoftBold, style: context.textTheme.titleMedium?.toSoftBold,
), ),
) )
.width + .width +
@@ -123,10 +123,12 @@ class _StartButtonState extends State<StartButton>
child: Consumer( child: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final runTime = ref.watch(runTimeProvider); final runTime = ref.watch(runTimeProvider);
final text = other.getTimeText(runTime); final text = utils.getTimeText(runTime);
return Text( return Text(
text, text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold, style: Theme.of(context).textTheme.titleMedium?.toSoftBold.copyWith(
color: context.colorScheme.onPrimaryContainer
),
); );
}, },
), ),

View File

@@ -11,7 +11,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
class TrafficUsage extends StatelessWidget { class TrafficUsage extends StatelessWidget {
const TrafficUsage({super.key}); const TrafficUsage({super.key});
Widget getTrafficDataItem( Widget _buildTrafficDataItem(
BuildContext context, BuildContext context,
Icon icon, Icon icon,
TrafficValue trafficValue, TrafficValue trafficValue,
@@ -51,10 +51,8 @@ class TrafficUsage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final primaryColor = final primaryColor = globalState.theme.darken3PrimaryContainer;
context.colorScheme.surfaceContainer.blendDarken(context, factor: 0.2); final secondaryColor = globalState.theme.darken2SecondaryContainer;
final secondaryColor =
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3);
return SizedBox( return SizedBox(
height: getWidgetHeight(2), height: getWidgetHeight(2),
child: CommonCard( child: CommonCard(
@@ -189,7 +187,7 @@ class TrafficUsage extends StatelessWidget {
), ),
), ),
), ),
getTrafficDataItem( _buildTrafficDataItem(
context, context,
Icon( Icon(
Icons.arrow_upward, Icons.arrow_upward,
@@ -201,7 +199,7 @@ class TrafficUsage extends StatelessWidget {
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
getTrafficDataItem( _buildTrafficDataItem(
context, context,
Icon( Icon(
Icons.arrow_downward, Icons.arrow_downward,

View File

@@ -4,6 +4,7 @@ import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart'; import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -156,9 +157,28 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return CommonDialog(
title: Text(IntlExt.actionMessage((widget.hotKeyAction.action.name))), title: IntlExt.actionMessage(widget.hotKeyAction.action.name),
content: ValueListenableBuilder( actions: [
TextButton(
onPressed: () {
_handleRemove();
},
child: Text(appLocalizations.remove),
),
const SizedBox(
width: 8,
),
TextButton(
onPressed: () {
_handleConfirm();
},
child: Text(
appLocalizations.confirm,
),
),
],
child: ValueListenableBuilder(
valueListenable: hotKeyActionNotifier, valueListenable: hotKeyActionNotifier,
builder: (_, hotKeyAction, ___) { builder: (_, hotKeyAction, ___) {
final key = hotKeyAction.key; final key = hotKeyAction.key;
@@ -191,25 +211,6 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
); );
}, },
), ),
actions: [
TextButton(
onPressed: () {
_handleRemove();
},
child: Text(appLocalizations.remove),
),
const SizedBox(
width: 8,
),
TextButton(
onPressed: () {
_handleConfirm();
},
child: Text(
appLocalizations.confirm,
),
),
],
); );
} }
} }

View File

@@ -8,8 +8,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../widgets/widgets.dart'; import '../widgets/widgets.dart';
double _preOffset = 0;
class LogsFragment extends ConsumerStatefulWidget { class LogsFragment extends ConsumerStatefulWidget {
const LogsFragment({super.key}); const LogsFragment({super.key});
@@ -19,17 +17,20 @@ class LogsFragment extends ConsumerStatefulWidget {
class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin { class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState()); final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final _scrollController = ScrollController( final _cacheKey = ValueKey("logs_list");
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite, late ScrollController _scrollController;
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0; double _currentMaxWidth = 0;
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
List<Log> _logs = []; List<Log> _logs = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1;
_scrollController = ScrollController(
initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite,
);
_logsStateNotifier.value = _logsStateNotifier.value.copyWith( _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: globalState.appState.logs.list, logs: globalState.appState.logs.list,
); );
@@ -90,14 +91,13 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
void dispose() { void dispose() {
_logsStateNotifier.dispose(); _logsStateNotifier.dispose();
_scrollController.dispose(); _scrollController.dispose();
_cacheDynamicHeightMap.clear();
super.dispose(); super.dispose();
} }
_handleTryClearCache(double maxWidth) { _handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) { if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth; _currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear(); _key.currentState?.clearCache();
} }
} }
@@ -116,27 +116,19 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
); );
} }
double _calcCacheHeight(String text) {
final cacheHeight = _cacheDynamicHeightMap.get(text);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize(
Text(
text,
style: globalState.appController.context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
);
_cacheDynamicHeightMap.put(text, size.height);
return size.height;
}
double _getItemHeight(Log log) { double _getItemHeight(Log log) {
final measure = globalState.measure; final measure = globalState.measure;
final bodySmallHeight = measure.bodySmallHeight; final bodySmallHeight = measure.bodySmallHeight;
final bodyMediumHeight = measure.bodyMediumHeight; final bodyMediumHeight = measure.bodyMediumHeight;
final height = _calcCacheHeight(log.payload ?? ""); final height = globalState.measure
.computeTextSize(
Text(
log.payload ?? "",
style: globalState.appController.context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
)
.height;
return height + bodySmallHeight + 8 + bodyMediumHeight + 40; return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
} }
@@ -189,14 +181,14 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
), ),
) )
.toList(); .toList();
return NotificationListener<ScrollEndNotification>( return ScrollToEndBox<Log>(
onNotification: (details) { controller: _scrollController,
_preOffset = details.metrics.pixels; cacheKey: _cacheKey,
return false; dataSource: logs,
},
child: CommonScrollBar( child: CommonScrollBar(
controller: _scrollController, controller: _scrollController,
child: ListView.builder( child: CacheItemExtentListView(
key: _key,
reverse: true, reverse: true,
shrinkWrap: true, shrinkWrap: true,
physics: NextClampingScrollPhysics(), physics: NextClampingScrollPhysics(),
@@ -204,7 +196,7 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
itemBuilder: (_, index) { itemBuilder: (_, index) {
return items[index]; return items[index];
}, },
itemExtentBuilder: (index, __) { itemExtentBuilder: (index) {
final item = items[index]; final item = items[index];
if (item.runtimeType == Divider) { if (item.runtimeType == Divider) {
return 0; return 0;
@@ -213,6 +205,14 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
return _getItemHeight(log); return _getItemHeight(log);
}, },
itemCount: items.length, itemCount: items.length,
keyBuilder: (int index) {
final item = items[index];
if (item.runtimeType == Divider) {
return "divider";
}
final log = logs[(index / 2).floor()];
return log.payload ?? "";
},
), ),
), ),
); );
@@ -272,11 +272,3 @@ class LogItem extends StatelessWidget {
); );
} }
} }
class NoGlowScrollBehavior extends ScrollBehavior {
@override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child; // 禁用过度滚动效果
}
}

View File

@@ -50,19 +50,19 @@ class AddProfile extends StatelessWidget {
return ListView( return ListView(
children: [ children: [
ListItem( ListItem(
leading: const Icon(Icons.qr_code), leading: const Icon(Icons.qr_code_sharp),
title: Text(appLocalizations.qrcode), title: Text(appLocalizations.qrcode),
subtitle: Text(appLocalizations.qrcodeDesc), subtitle: Text(appLocalizations.qrcodeDesc),
onTap: _toScan, onTap: _toScan,
), ),
ListItem( ListItem(
leading: const Icon(Icons.upload_file), leading: const Icon(Icons.upload_file_sharp),
title: Text(appLocalizations.file), title: Text(appLocalizations.file),
subtitle: Text(appLocalizations.fileDesc), subtitle: Text(appLocalizations.fileDesc),
onTap: _handleAddProfileFormFile, onTap: _handleAddProfileFormFile,
), ),
ListItem( ListItem(
leading: const Icon(Icons.cloud_download), leading: const Icon(Icons.cloud_download_sharp),
title: Text(appLocalizations.url), title: Text(appLocalizations.url),
subtitle: Text(appLocalizations.urlDesc), subtitle: Text(appLocalizations.urlDesc),
onTap: _toAdd, onTap: _toAdd,
@@ -90,9 +90,15 @@ class _URLFormDialogState extends State<URLFormDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return CommonDialog(
title: Text(appLocalizations.importFromURL), title: appLocalizations.importFromURL,
content: SizedBox( actions: [
TextButton(
onPressed: _handleAddProfileFormURL,
child: Text(appLocalizations.submit),
)
],
child: SizedBox(
width: 300, width: 300,
child: Wrap( child: Wrap(
runSpacing: 16, runSpacing: 16,
@@ -109,12 +115,6 @@ class _URLFormDialogState extends State<URLFormDialog> {
], ],
), ),
), ),
actions: [
TextButton(
onPressed: _handleAddProfileFormURL,
child: Text(appLocalizations.submit),
)
],
); );
} }
} }

View File

@@ -1,54 +0,0 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
class CustomProfile extends StatefulWidget {
final String profileId;
const CustomProfile({
super.key,
required this.profileId,
});
@override
State<CustomProfile> createState() => _CustomProfileState();
}
class _CustomProfileState extends State<CustomProfile> {
final _currentClashConfigNotifier = ValueNotifier<ClashConfig?>(null);
@override
void initState() {
super.initState();
_initCurrentClashConfig();
}
_initCurrentClashConfig() async {
// final currentProfileId = globalState.config.currentProfileId;
// if (currentProfileId == null) {
// return;
// }
// _currentClashConfigNotifier.value =
// await clashCore.getProfile(currentProfileId);
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
body: ValueListenableBuilder(
valueListenable: _currentClashConfigNotifier,
builder: (_, clashConfig, ___) {
if (clashConfig == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return Column(
children: [],
);
},
),
title: "自定义",
);
}
}

View File

@@ -80,9 +80,9 @@ class _EditProfileState extends State<EditProfile> {
); );
} }
} }
appController.setProfile(await profile.saveFile(fileData!)); appController.setProfileAndAutoApply(await profile.saveFile(fileData!));
} else if (!hasUpdate) { } else if (!hasUpdate) {
appController.setProfile(profile); appController.setProfileAndAutoApply(profile);
} else { } else {
globalState.homeScaffoldKey.currentState?.loadingRun( globalState.homeScaffoldKey.currentState?.loadingRun(
() async { () async {
@@ -282,7 +282,7 @@ class _EditProfileState extends State<EditProfile> {
ValueListenableBuilder<FileInfo?>( ValueListenableBuilder<FileInfo?>(
valueListenable: fileInfoNotifier, valueListenable: fileInfoNotifier,
builder: (_, fileInfo, __) { builder: (_, fileInfo, __) {
return FadeBox( return FadeThroughBox(
child: fileInfo == null child: fileInfo == null
? Container() ? Container()
: ListItem( : ListItem(
@@ -324,15 +324,13 @@ class _EditProfileState extends State<EditProfile> {
}, },
), ),
]; ];
return PopScope( return CommonPopScope(
canPop: false, onPop: () {
onPopInvokedWithResult: (didPop, __) {
if (didPop) return;
if (fileData == null) { if (fileData == null) {
Navigator.of(context).pop(); return true;
return;
} }
_handleBack(); _handleBack();
return false;
}, },
child: FloatLayout( child: FloatLayout(
floatingWidget: FloatWrapper( floatingWidget: FloatWrapper(

View File

@@ -0,0 +1,958 @@
import 'dart:ui';
import 'package:fl_clash/clash/core.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';
class OverrideProfile extends StatefulWidget {
final String profileId;
const OverrideProfile({
super.key,
required this.profileId,
});
@override
State<OverrideProfile> createState() => _OverrideProfileState();
}
class _OverrideProfileState extends State<OverrideProfile> {
final GlobalKey<CacheItemExtentListViewState> _ruleListKey = GlobalKey();
final _controller = ScrollController();
double _currentMaxWidth = 0;
_initState(WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(milliseconds: 300), () async {
final snippet = await clashCore.getProfile(widget.profileId);
final overrideData = ref.read(
getProfileOverrideDataProvider(widget.profileId),
);
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith(
snippet: snippet,
overrideData: overrideData,
),
);
});
});
}
_handleSave(WidgetRef ref, OverrideData overrideData) {
ref.read(needApplyProvider.notifier).value = true;
ref.read(profilesProvider.notifier).updateProfile(
widget.profileId,
(state) => state.copyWith(
overrideData: overrideData,
),
);
}
_handleDelete(WidgetRef ref) async {
final res = await globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.deleteRuleTip),
);
if (res != true) {
return;
}
final selectedRules = ref.read(
profileOverrideStateProvider.select(
(state) => state.selectedRules,
),
);
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) {
final overrideRule = state.overrideData!.rule.updateRules(
(rules) => List.from(
rules.where(
(item) => !selectedRules.contains(item.id),
),
),
);
return state.copyWith.overrideData!(
rule: overrideRule,
);
},
);
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith(isEdit: false, selectedRules: {}),
);
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_ruleListKey.currentState?.clearCache();
}
}
_buildContent() {
return Consumer(
builder: (_, ref, child) {
final isInit = ref.watch(
profileOverrideStateProvider.select(
(state) => state.snippet != null && state.overrideData != null,
),
);
if (!isInit) {
return Center(
child: CircularProgressIndicator(),
);
}
return FadeBox(
child: !isInit
? Center(
child: CircularProgressIndicator(),
)
: child!,
);
},
child: LayoutBuilder(
builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 104);
return CommonAutoHiddenScrollBar(
controller: _controller,
child: CustomScrollView(
controller: _controller,
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 16,
),
),
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: OverrideSwitch(),
),
),
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.only(
left: 8,
right: 8,
),
child: RuleTitle(
profileId: widget.profileId,
),
),
),
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 0),
sliver: RuleContent(
maxWidth: _currentMaxWidth,
ruleListKey: _ruleListKey,
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 16,
),
),
],
),
);
},
),
);
}
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
profileOverrideStateProvider.overrideWith(() => ProfileOverrideState()),
],
child: Consumer(
builder: (_, ref, child) {
_initState(ref);
return child!;
},
child: Consumer(
builder: (_, ref, ___) {
final vm2 = ref.watch(
profileOverrideStateProvider.select(
(state) => VM2(
a: state.isEdit,
b: state.selectedRules.length,
),
),
);
final isEdit = vm2.a;
final editCount = vm2.b;
return CommonScaffold(
title: appLocalizations.override,
body: _buildContent(),
actions: [
if (!isEdit)
Consumer(
builder: (_, ref, child) {
final overrideData = ref.watch(
getProfileOverrideDataProvider(widget.profileId));
final newOverrideData = ref.watch(
profileOverrideStateProvider.select(
(state) => state.overrideData,
),
);
final equals = overrideData == newOverrideData;
if (equals || newOverrideData == null) {
return SizedBox();
}
return CommonPopScope(
onPop: () async {
if (equals) {
return true;
}
final res = await globalState.showMessage(
message: TextSpan(
text: appLocalizations.saveChanges,
),
confirmText: appLocalizations.save,
);
if (!context.mounted || res != true) {
return true;
}
_handleSave(ref, newOverrideData);
return true;
},
child: IconButton(
onPressed: () async {
final res = await globalState.showMessage(
message: TextSpan(
text: appLocalizations.saveTip,
),
confirmText: appLocalizations.tip,
);
if (res != true) {
return;
}
_handleSave(ref, newOverrideData);
},
icon: Icon(
Icons.save,
),
),
);
},
),
if (editCount == 1)
IconButton(
onPressed: () {
final rule = ref.read(profileOverrideStateProvider.select(
(state) {
return state.overrideData?.rule.rules.firstWhere(
(item) => item.id == state.selectedRules.first,
);
},
));
if (rule == null) {
return;
}
globalState.appController.handleAddOrUpdate(
ref,
rule,
);
},
icon: Icon(
Icons.edit,
),
),
if (editCount > 0)
IconButton(
onPressed: () {
_handleDelete(ref);
},
icon: Icon(
Icons.delete,
),
)
],
appBarEditState: AppBarEditState(
isEdit: isEdit,
editCount: editCount,
onExit: () {
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith(
isEdit: false,
selectedRules: {},
),
);
},
),
);
},
),
),
);
}
}
class OverrideSwitch extends ConsumerWidget {
const OverrideSwitch({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final enable = ref.watch(
profileOverrideStateProvider.select(
(state) => state.overrideData?.enable,
),
);
return CommonCard(
onPressed: () {},
type: CommonCardType.filled,
radius: 18,
child: ListItem.switchItem(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 4,
bottom: 4,
),
title: Text(appLocalizations.enableOverride),
delegate: SwitchDelegate(
value: enable ?? false,
onChanged: (value) {
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith.overrideData!(
enable: value,
),
);
},
),
),
);
}
}
class RuleTitle extends ConsumerWidget {
final String profileId;
const RuleTitle({
super.key,
required this.profileId,
});
_handleChangeType(WidgetRef ref, isOverrideRule) {
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith.overrideData!.rule(
type: isOverrideRule
? OverrideRuleType.added
: OverrideRuleType.override,
),
);
}
@override
Widget build(BuildContext context, ref) {
final vm3 = ref.watch(
profileOverrideStateProvider.select(
(state) {
final overrideRule = state.overrideData?.rule;
return VM3(
a: state.isEdit,
b: state.selectedRules.containsAll(
overrideRule?.rules.map((item) => item.id).toSet() ?? {},
),
c: overrideRule?.type == OverrideRuleType.override,
);
},
),
);
final isEdit = vm3.a;
final isSelectAll = vm3.b;
final isOverrideRule = vm3.c;
return FilledButtonTheme(
data: FilledButtonThemeData(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.symmetric(
horizontal: 8,
)),
visualDensity: VisualDensity.compact,
),
),
child: IconButtonTheme(
data: IconButtonThemeData(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.zero),
visualDensity: VisualDensity.compact,
iconSize: WidgetStatePropertyAll(20),
),
),
child: ListHeader(
title: appLocalizations.rule,
subTitle: isOverrideRule
? appLocalizations.overrideOriginRules
: appLocalizations.addedOriginRules,
space: 8,
actions: [
if (!isEdit)
IconButton.filledTonal(
icon: Icon(
isOverrideRule ? Icons.edit_document : Icons.note_add,
),
onPressed: () {
_handleChangeType(
ref,
isOverrideRule,
);
},
),
!isEdit
? FilledButton.tonal(
onPressed: () {
globalState.appController.handleAddOrUpdate(ref);
},
child: Text(appLocalizations.add),
)
: isSelectAll
? FilledButton(
onPressed: () {
ref
.read(profileOverrideStateProvider.notifier)
.updateState(
(state) => state.copyWith(
selectedRules: {},
),
);
},
child: Text(appLocalizations.selectAll),
)
: FilledButton.tonal(
onPressed: () {
ref
.read(profileOverrideStateProvider.notifier)
.updateState(
(state) => state.copyWith(
selectedRules: state.overrideData?.rule.rules
.map((item) => item.id)
.toSet() ??
{},
),
);
},
child: Text(appLocalizations.selectAll),
),
],
),
),
);
}
}
class RuleContent extends ConsumerWidget {
final Key ruleListKey;
final double maxWidth;
const RuleContent({
super.key,
required this.ruleListKey,
required this.maxWidth,
});
Widget _proxyDecorator(
Widget child,
int index,
Animation<double> animation,
) {
return AnimatedBuilder(
animation: animation,
builder: (_, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
child: child,
);
},
child: child,
);
}
Widget _buildItem(Rule rule, int index) {
return Consumer(
builder: (context, ref, ___) {
final vm2 = ref.watch(profileOverrideStateProvider.select(
(item) => VM2(
a: item.isEdit,
b: item.selectedRules.contains(rule.id),
),
));
final isEdit = vm2.a;
final isSelected = vm2.b;
return Material(
color: Colors.transparent,
child: Container(
margin: EdgeInsets.symmetric(
vertical: 4,
),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? context.colorScheme.secondaryContainer.opacity80
: context.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
clipBehavior: Clip.hardEdge,
child: ListTile(
minTileHeight: 0,
minVerticalPadding: 0,
titleTextStyle: context.textTheme.bodyMedium?.toJetBrainsMono,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
trailing: SizedBox(
width: 24,
height: 24,
child: !isEdit
? ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
)
: CommonCheckBox(
value: isSelected,
isCircle: true,
onChanged: (_) {
_handleSelect(ref, rule);
},
),
),
title: Text(rule.value),
),
),
);
},
);
}
_handleSelect(WidgetRef ref, ruleId) {
if (!ref.read(profileOverrideStateProvider).isEdit) {
return;
}
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) {
final newSelectedRules = Set<String>.from(state.selectedRules);
if (newSelectedRules.contains(ruleId)) {
newSelectedRules.remove(ruleId);
} else {
newSelectedRules.add(ruleId);
}
return state.copyWith(
selectedRules: newSelectedRules,
);
},
);
}
@override
Widget build(BuildContext context, ref) {
final vm2 = ref.watch(
profileOverrideStateProvider.select(
(state) {
final overrideRule = state.overrideData?.rule;
return VM2(
a: overrideRule?.rules ?? [],
b: overrideRule?.type ?? OverrideRuleType.added,
);
},
),
);
final rules = vm2.a;
final type = vm2.b;
if (rules.isEmpty) {
return SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: Center(
child: type == OverrideRuleType.added
? Text(
appLocalizations.noData,
)
: FilledButton(
onPressed: () {
final rules = ref.read(
profileOverrideStateProvider.select(
(state) => state.snippet?.rule ?? [],
),
);
ref
.read(profileOverrideStateProvider.notifier)
.updateState(
(state) {
return state.copyWith.overrideData!.rule(
overrideRules: rules,
);
},
);
},
child: Text(appLocalizations.getOriginRules),
),
),
),
);
}
return CacheItemExtentSliverReorderableList(
key: ruleListKey,
itemBuilder: (context, index) {
final rule = rules[index];
return GestureDetector(
key: ObjectKey(rule),
child: _buildItem(
rule,
index,
),
onTap: () {
_handleSelect(ref, rule.id);
},
onLongPress: () {
if (ref.read(profileOverrideStateProvider).isEdit) {
return;
}
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith(
isEdit: true,
selectedRules: {
rule.id,
},
),
);
},
);
},
proxyDecorator: _proxyDecorator,
itemCount: rules.length,
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final newRules = List<Rule>.from(rules);
final item = newRules.removeAt(oldIndex);
newRules.insert(newIndex, item);
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith.overrideData!(
rule: state.overrideData!.rule.updateRules((_) => newRules),
),
);
},
keyBuilder: (int index) {
return rules[index].value;
},
itemExtentBuilder: (index) {
final rule = rules[index];
return 40 +
globalState.measure
.computeTextSize(
Text(
rule.value,
style: context.textTheme.bodyMedium?.toJetBrainsMono,
),
maxWidth: maxWidth,
)
.height;
},
);
}
}
class AddRuleDialog extends StatefulWidget {
final ClashConfigSnippet snippet;
final Rule? rule;
const AddRuleDialog({
super.key,
required this.snippet,
this.rule,
});
@override
State<AddRuleDialog> createState() => _AddRuleDialogState();
}
class _AddRuleDialogState extends State<AddRuleDialog> {
late RuleAction _ruleAction;
final _ruleTargetController = TextEditingController();
final _contentController = TextEditingController();
final _ruleProviderController = TextEditingController();
final _subRuleController = TextEditingController();
bool _noResolve = false;
bool _src = false;
List<DropdownMenuEntry> _targetItems = [];
List<DropdownMenuEntry> _ruleProviderItems = [];
List<DropdownMenuEntry> _subRuleItems = [];
final _formKey = GlobalKey<FormState>();
@override
void initState() {
_initState();
super.initState();
}
_initState() {
_targetItems = [
...widget.snippet.proxyGroups.map(
(item) => DropdownMenuEntry(
value: item.name,
label: item.name,
),
),
...RuleTarget.values.map(
(item) => DropdownMenuEntry(
value: item.name,
label: item.name,
),
),
];
_ruleProviderItems = [
...widget.snippet.ruleProvider.map(
(item) => DropdownMenuEntry(
value: item.name,
label: item.name,
),
),
];
_subRuleItems = [
...widget.snippet.subRules.map(
(item) => DropdownMenuEntry(
value: item.name,
label: item.name,
),
),
];
if (widget.rule != null) {
final parsedRule = ParsedRule.parseString(widget.rule!.value);
_ruleAction = parsedRule.ruleAction;
_contentController.text = parsedRule.content ?? "";
_ruleTargetController.text = parsedRule.ruleTarget ?? "";
_ruleProviderController.text = parsedRule.ruleProvider ?? "";
_subRuleController.text = parsedRule.subRule ?? "";
_noResolve = parsedRule.noResolve;
_src = parsedRule.src;
return;
}
_ruleAction = RuleAction.values.first;
if (_targetItems.isNotEmpty) {
_ruleTargetController.text = _targetItems.first.value;
}
if (_ruleProviderItems.isNotEmpty) {
_ruleProviderController.text = _ruleProviderItems.first.value;
}
if (_subRuleItems.isNotEmpty) {
_subRuleController.text = _subRuleItems.first.value;
}
}
@override
void didUpdateWidget(AddRuleDialog oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.rule != widget.rule) {
_initState();
}
}
_handleSubmit() {
final res = _formKey.currentState?.validate();
if (res == false) {
return;
}
final parsedRule = ParsedRule(
ruleAction: _ruleAction,
content: _contentController.text,
ruleProvider: _ruleProviderController.text,
ruleTarget: _ruleTargetController.text,
subRule: _subRuleController.text,
noResolve: _noResolve,
src: _src,
);
final rule = widget.rule != null
? widget.rule!.copyWith(value: parsedRule.value)
: Rule.value(
parsedRule.value,
);
Navigator.of(context).pop(rule);
}
@override
Widget build(BuildContext context) {
return CommonDialog(
title: appLocalizations.addRule,
actions: [
TextButton(
onPressed: _handleSubmit,
child: Text(
appLocalizations.confirm,
),
),
],
child: DropdownMenuTheme(
data: DropdownMenuThemeData(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(),
labelStyle: context.textTheme.bodyLarge
?.copyWith(overflow: TextOverflow.ellipsis),
),
),
child: Form(
key: _formKey,
child: LayoutBuilder(
builder: (_, constraints) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FilledButton.tonal(
onPressed: () async {
_ruleAction =
await globalState.showCommonDialog<RuleAction>(
child: OptionsDialog<RuleAction>(
title: appLocalizations.ruleName,
options: RuleAction.values,
textBuilder: (item) => item.value,
value: _ruleAction,
),
) ??
_ruleAction;
setState(() {});
},
child: Text(_ruleAction.name),
),
SizedBox(
height: 24,
),
_ruleAction == RuleAction.RULE_SET
? FormField(
validator: (_) {
if (_ruleProviderController.text.isEmpty) {
return appLocalizations.ruleProviderEmptyTip;
}
return null;
},
builder: (field) {
return DropdownMenu(
expandedInsets: EdgeInsets.zero,
controller: _ruleProviderController,
label: Text(appLocalizations.ruleProviders),
menuHeight: 250,
errorText: field.errorText,
dropdownMenuEntries: _ruleProviderItems,
);
},
)
: TextFormField(
controller: _contentController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: appLocalizations.content,
),
validator: (_) {
if (_contentController.text.isEmpty) {
return appLocalizations.contentEmptyTip;
}
return null;
},
),
SizedBox(
height: 24,
),
_ruleAction == RuleAction.SUB_RULE
? FormField(
validator: (_) {
if (_subRuleController.text.isEmpty) {
return appLocalizations.subRuleEmptyTip;
}
return null;
},
builder: (filed) {
return DropdownMenu(
width: 200,
controller: _subRuleController,
label: Text(appLocalizations.subRule),
menuHeight: 250,
dropdownMenuEntries: _subRuleItems,
);
},
)
: FormField<String>(
validator: (_) {
if (_ruleTargetController.text.isEmpty) {
return appLocalizations.ruleTargetEmptyTip;
}
return null;
},
builder: (filed) {
return DropdownMenu(
controller: _ruleTargetController,
initialSelection: filed.value,
label: Text(appLocalizations.ruleTarget),
width: 200,
menuHeight: 250,
enableFilter: true,
dropdownMenuEntries: _targetItems,
errorText: filed.errorText,
);
},
),
if (_ruleAction.hasParams) ...[
SizedBox(
height: 20,
),
Wrap(
spacing: 8,
children: [
CommonCard(
radius: 8,
isSelected: _src,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
child: Text(
appLocalizations.sourceIp,
style: context.textTheme.bodyMedium,
),
),
onPressed: () {
setState(() {
_src = !_src;
});
},
),
CommonCard(
radius: 8,
isSelected: _noResolve,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
child: Text(
appLocalizations.noResolve,
style: context.textTheme.bodyMedium,
),
),
onPressed: () {
setState(() {
_noResolve = !_noResolve;
});
},
)
],
),
],
SizedBox(
height: 20,
),
],
);
},
),
),
),
);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/fragments/profiles/override_profile.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -23,12 +24,17 @@ class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
Function? applyConfigDebounce; Function? applyConfigDebounce;
_handleShowAddExtendPage() { _handleShowAddExtendPage() {
showExtendPage( showExtend(
globalState.navigatorKey.currentState!.context, globalState.navigatorKey.currentState!.context,
body: AddProfile( builder: (_, type) {
context: globalState.navigatorKey.currentState!.context, return AdaptiveSheetScaffold(
), type: type,
title: "${appLocalizations.add}${appLocalizations.profile}", body: AddProfile(
context: globalState.navigatorKey.currentState!.context,
),
title: "${appLocalizations.add}${appLocalizations.profile}",
);
},
); );
} }
@@ -80,12 +86,13 @@ class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
onPressed: () { onPressed: () {
final profiles = globalState.config.profiles; final profiles = globalState.config.profiles;
showSheet( showSheet(
title: appLocalizations.profilesSort,
context: context, context: context,
body: SizedBox( builder: (_, type) {
height: 400, return ReorderableProfilesSheet(
child: ReorderableProfiles(profiles: profiles), type: type,
), profiles: profiles,
);
},
); );
}, },
icon: const Icon(Icons.sort), icon: const Icon(Icons.sort),
@@ -181,10 +188,6 @@ class ProfileItem extends StatelessWidget {
await globalState.appController.deleteProfile(profile.id); await globalState.appController.deleteProfile(profile.id);
} }
_handleUpdateProfile() async {
await globalState.safeRun<void>(updateProfile);
}
Future updateProfile() async { Future updateProfile() async {
final appController = globalState.appController; final appController = globalState.appController;
if (profile.type == ProfileType.file) return; if (profile.type == ProfileType.file) return;
@@ -208,13 +211,18 @@ class ProfileItem extends StatelessWidget {
} }
_handleShowEditExtendPage(BuildContext context) { _handleShowEditExtendPage(BuildContext context) {
showExtendPage( showExtend(
context, context,
body: EditProfile( builder: (_, type) {
profile: profile, return AdaptiveSheetScaffold(
context: context, type: type,
), body: EditProfile(
title: "${appLocalizations.edit}${appLocalizations.profile}", profile: profile,
context: context,
),
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
},
); );
} }
@@ -277,18 +285,17 @@ class ProfileItem extends StatelessWidget {
} }
} }
// _handlePushCustomPage(BuildContext context, String id) { _handlePushGenProfilePage(BuildContext context, String id) {
// BaseNavigator.push( BaseNavigator.push(
// context, context,
// CustomProfile( OverrideProfile(
// profileId: id, profileId: id,
// ), ),
// ); );
// } }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final key = GlobalKey<CommonPopupBoxState>();
return CommonCard( return CommonCard(
isSelected: profile.id == groupValue, isSelected: profile.id == groupValue,
onPressed: () { onPressed: () {
@@ -301,17 +308,16 @@ class ProfileItem extends StatelessWidget {
trailing: SizedBox( trailing: SizedBox(
height: 40, height: 40,
width: 40, width: 40,
child: FadeBox( child: FadeThroughBox(
child: profile.isUpdating child: profile.isUpdating
? const Padding( ? const Padding(
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
: CommonPopupBox( : CommonPopupBox(
key: key,
popup: CommonPopupMenu( popup: CommonPopupMenu(
items: [ items: [
ActionItemData( PopupMenuItemData(
icon: Icons.edit_outlined, icon: Icons.edit_outlined,
label: appLocalizations.edit, label: appLocalizations.edit,
onPressed: () { onPressed: () {
@@ -319,52 +325,47 @@ class ProfileItem extends StatelessWidget {
}, },
), ),
if (profile.type == ProfileType.url) ...[ if (profile.type == ProfileType.url) ...[
ActionItemData( PopupMenuItemData(
icon: Icons.sync_alt_sharp, icon: Icons.sync_alt_sharp,
label: appLocalizations.sync, label: appLocalizations.sync,
onPressed: () { onPressed: () {
_handleUpdateProfile(); updateProfile();
}, },
), ),
// ActionItemData(
// icon: Icons.copy,
// label: appLocalizations.copyLink,
// onPressed: () {
// _handleCopyLink(context);
// },
// ),
], ],
// ActionItemData( PopupMenuItemData(
// icon: Icons.extension_outlined, icon: Icons.extension_outlined,
// label: "自定义", label: appLocalizations.override,
// onPressed: () { onPressed: () {
// _handlePushCustomPage(context, profile.id); _handlePushGenProfilePage(context, profile.id);
// }, },
// ), ),
ActionItemData( PopupMenuItemData(
icon: Icons.file_copy_outlined, icon: Icons.file_copy_outlined,
label: appLocalizations.exportFile, label: appLocalizations.exportFile,
onPressed: () { onPressed: () {
_handleExportFile(context); _handleExportFile(context);
}, },
), ),
ActionItemData( PopupMenuItemData(
icon: Icons.delete_outlined, icon: Icons.delete_outlined,
iconSize: 20, iconSize: 20,
label: appLocalizations.delete, label: appLocalizations.delete,
onPressed: () { onPressed: () {
_handleDeleteProfile(context); _handleDeleteProfile(context);
}, },
type: ActionType.danger, type: PopupMenuItemType.danger,
), ),
], ],
), ),
target: IconButton( targetBuilder: (open) {
onPressed: () { return IconButton(
key.currentState?.pop(); onPressed: () {
}, open();
icon: Icon(Icons.more_vert), },
), icon: Icon(Icons.more_vert),
);
},
), ),
), ),
), ),
@@ -400,19 +401,22 @@ class ProfileItem extends StatelessWidget {
} }
} }
class ReorderableProfiles extends StatefulWidget { class ReorderableProfilesSheet extends StatefulWidget {
final List<Profile> profiles; final List<Profile> profiles;
final SheetType type;
const ReorderableProfiles({ const ReorderableProfilesSheet({
super.key, super.key,
required this.profiles, required this.profiles,
required this.type,
}); });
@override @override
State<ReorderableProfiles> createState() => _ReorderableProfilesState(); State<ReorderableProfilesSheet> createState() =>
_ReorderableProfilesSheetState();
} }
class _ReorderableProfilesState extends State<ReorderableProfiles> { class _ReorderableProfilesSheetState extends State<ReorderableProfilesSheet> {
late List<Profile> profiles; late List<Profile> profiles;
@override @override
@@ -456,74 +460,61 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return AdaptiveSheetScaffold(
mainAxisSize: MainAxisSize.min, type: widget.type,
children: [ actions: [
Expanded( IconButton(
flex: 1, onPressed: () {
child: ReorderableListView.builder( Navigator.of(context).pop();
buildDefaultDragHandles: false, globalState.appController.setProfiles(profiles);
padding: const EdgeInsets.symmetric(horizontal: 12), },
proxyDecorator: proxyDecorator, icon: Icon(
onReorder: (oldIndex, newIndex) { Icons.save,
setState(() { ),
if (oldIndex < newIndex) { )
newIndex -= 1; ],
} body: Padding(
final profile = profiles.removeAt(oldIndex); padding: EdgeInsets.only(bottom: 32),
profiles.insert(newIndex, profile); child: ReorderableListView.builder(
}); buildDefaultDragHandles: false,
}, padding: const EdgeInsets.symmetric(
itemBuilder: (_, index) { horizontal: 12,
final profile = profiles[index]; ),
return Container( proxyDecorator: proxyDecorator,
key: Key(profile.id), onReorder: (oldIndex, newIndex) {
padding: const EdgeInsets.symmetric(vertical: 4), setState(() {
child: CommonCard( if (oldIndex < newIndex) {
type: CommonCardType.filled, newIndex -= 1;
child: ListTile( }
contentPadding: const EdgeInsets.only( final profile = profiles.removeAt(oldIndex);
right: 16, profiles.insert(newIndex, profile);
left: 16, });
), },
title: Text(profile.label ?? profile.id), itemBuilder: (_, index) {
trailing: ReorderableDragStartListener( final profile = profiles[index];
index: index, return Container(
child: const Icon(Icons.drag_handle), key: Key(profile.id),
), padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 16,
left: 16,
),
title: Text(profile.label ?? profile.id),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
), ),
), ),
);
},
itemCount: profiles.length,
),
),
Container(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 24,
),
child: FilledButton.tonal(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.setProfiles(profiles);
},
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(vertical: 8),
), ),
), );
child: Row( },
mainAxisAlignment: MainAxisAlignment.center, itemCount: profiles.length,
children: [
Text(
appLocalizations.confirm,
),
],
),
),
), ),
], ),
title: appLocalizations.profilesSort,
); );
} }
} }

View File

@@ -63,7 +63,7 @@ class ProxyCard extends StatelessWidget {
delay > 0 ? '$delay ms' : "Timeout", delay > 0 ? '$delay ms' : "Timeout",
style: context.textTheme.labelSmall?.copyWith( style: context.textTheme.labelSmall?.copyWith(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: other.getDelayColor( color: utils.getDelayColor(
delay, delay,
), ),
), ),
@@ -178,7 +178,7 @@ class ProxyCard extends StatelessWidget {
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: color:
context.textTheme.bodySmall?.color?.toLight, context.textTheme.bodySmall?.color?.opacity80,
), ),
), ),
), ),
@@ -221,7 +221,7 @@ class _ProxyDesc extends ConsumerWidget {
desc, desc,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: context.textTheme.bodySmall?.color?.toLight, color: context.textTheme.bodySmall?.color?.opacity80,
), ),
); );
} }

View File

@@ -22,44 +22,52 @@ double getItemHeight(ProxyCardType proxyCardType) {
proxyDelayTest(Proxy proxy, [String? testUrl]) async { proxyDelayTest(Proxy proxy, [String? testUrl]) async {
final appController = globalState.appController; final appController = globalState.appController;
final proxyName = appController.getRealProxyName(proxy.name); final state = appController.getProxyCardState(proxy.name);
final url = appController.getRealTestUrl(testUrl); final url = state.testUrl.getSafeValue(
appController.getRealTestUrl(testUrl),
);
if (state.proxyName.isEmpty) {
return;
}
appController.setDelay( appController.setDelay(
Delay( Delay(
url: url, url: url,
name: proxyName, name: state.proxyName,
value: 0, value: 0,
), ),
); );
appController.setDelay( appController.setDelay(
await clashCore.getDelay( await clashCore.getDelay(
url, url,
proxyName, state.proxyName,
), ),
); );
} }
delayTest(List<Proxy> proxies, [String? testUrl]) async { delayTest(List<Proxy> proxies, [String? testUrl]) async {
final appController = globalState.appController; final appController = globalState.appController;
final proxyNames = proxies final proxyNames = proxies.map((proxy) => proxy.name).toSet().toList();
.map((proxy) => appController.getRealProxyName(proxy.name))
.toSet()
.toList();
final url = appController.getRealTestUrl(testUrl);
final delayProxies = proxyNames.map<Future>((proxyName) async { final delayProxies = proxyNames.map<Future>((proxyName) async {
final state = appController.getProxyCardState(proxyName);
final url = state.testUrl.getSafeValue(
appController.getRealTestUrl(testUrl),
);
final name = state.proxyName;
if (name.isEmpty) {
return;
}
appController.setDelay( appController.setDelay(
Delay( Delay(
url: url, url: url,
name: proxyName, name: name,
value: 0, value: 0,
), ),
); );
appController.setDelay( appController.setDelay(
await clashCore.getDelay( await clashCore.getDelay(
url, url,
proxyName, name,
), ),
); );
}).toList(); }).toList();

View File

@@ -467,10 +467,6 @@ class _ListHeaderState extends State<ListHeader>
return CommonCard( return CommonCard(
enterAnimated: widget.enterAnimated, enterAnimated: widget.enterAnimated,
key: widget.key, key: widget.key,
borderSide: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(
context.colorScheme.surfaceContainer,
),
radius: 14, radius: 14,
type: CommonCardType.filled, type: CommonCardType.filled,
child: Padding( child: Padding(
@@ -556,6 +552,7 @@ class _ListHeaderState extends State<ListHeader>
children: [ children: [
if (isExpand) ...[ if (isExpand) ...[
IconButton( IconButton(
visualDensity: VisualDensity.standard,
onPressed: () { onPressed: () {
widget.onScrollToSelected(groupName); widget.onScrollToSelected(groupName);
}, },
@@ -565,12 +562,13 @@ class _ListHeaderState extends State<ListHeader>
), ),
IconButton( IconButton(
onPressed: _delayTest, onPressed: _delayTest,
visualDensity: VisualDensity.standard,
icon: const Icon( icon: const Icon(
Icons.network_ping, Icons.network_ping,
), ),
), ),
const SizedBox( const SizedBox(
width: 4, width: 6,
), ),
], ],
AnimatedBuilder( AnimatedBuilder(

View File

@@ -13,8 +13,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
typedef UpdatingMap = Map<String, bool>; typedef UpdatingMap = Map<String, bool>;
class ProvidersView extends ConsumerStatefulWidget { class ProvidersView extends ConsumerStatefulWidget {
final SheetType type;
const ProvidersView({ const ProvidersView({
super.key, super.key,
required this.type,
}); });
@override @override
@@ -22,25 +25,6 @@ class ProvidersView extends ConsumerStatefulWidget {
} }
class _ProvidersViewState extends ConsumerState<ProvidersView> { class _ProvidersViewState extends ConsumerState<ProvidersView> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) {
globalState.appController.updateProviders();
context.commonScaffoldState?.actions = [
IconButton(
onPressed: () {
_updateProviders();
},
icon: const Icon(
Icons.sync,
),
)
];
},
);
}
_updateProviders() async { _updateProviders() async {
final providers = ref.read(providersProvider); final providers = ref.read(providersProvider);
@@ -102,10 +86,24 @@ class _ProvidersViewState extends ConsumerState<ProvidersView> {
title: appLocalizations.ruleProviders, title: appLocalizations.ruleProviders,
items: ruleProviders, items: ruleProviders,
); );
return generateListView([ return AdaptiveSheetScaffold(
...proxySection, actions: [
...ruleSection, IconButton(
]); onPressed: () {
_updateProviders();
},
icon: const Icon(
Icons.sync,
),
)
],
type: widget.type,
body: generateListView([
...proxySection,
...ruleSection,
]),
title: appLocalizations.providers,
);
} }
} }
@@ -222,7 +220,7 @@ class ProviderItem extends StatelessWidget {
trailing: SizedBox( trailing: SizedBox(
height: 48, height: 48,
width: 48, width: 48,
child: FadeBox( child: FadeThroughBox(
child: provider.isUpdating child: provider.isUpdating
? const Padding( ? const Padding(
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),

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