Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9cd80bcb3 | ||
|
|
a77b3a35e8 | ||
|
|
2d2708d7bd | ||
|
|
ef5f6dbd59 | ||
|
|
b6c7b15e3e |
56
.github/workflows/build.yaml
vendored
56
.github/workflows/build.yaml
vendored
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
env:
|
||||
IS_STABLE: ${{ !contains(github.ref, '-') }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -25,29 +27,27 @@ jobs:
|
||||
- platform: macos
|
||||
os: macos-latest
|
||||
arch: arm64
|
||||
- platform: windows
|
||||
os: windows-11-arm
|
||||
arch: arm64
|
||||
- platform: linux
|
||||
os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
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
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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
|
||||
if: startsWith(matrix.platform,'android')
|
||||
run: |
|
||||
@@ -56,25 +56,24 @@ jobs:
|
||||
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
|
||||
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
|
||||
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.24.0'
|
||||
cache-dependency-path: |
|
||||
core/go.sum
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
|
||||
cache: true
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
run: flutter pub get
|
||||
|
||||
- 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
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -88,14 +87,13 @@ jobs:
|
||||
needs: [ build ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
uses: actions/checkout@v4
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Generate
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
|
||||
@@ -127,7 +125,7 @@ jobs:
|
||||
cat NEW_CHANGELOG.md > CHANGELOG.md
|
||||
|
||||
- name: Commit
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
git add CHANGELOG.md
|
||||
if ! git diff --cached --quiet; then
|
||||
@@ -206,7 +204,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
python release.py
|
||||
python release_telegram.py
|
||||
|
||||
- name: Patch release.md
|
||||
run: |
|
||||
@@ -214,21 +212,21 @@ jobs:
|
||||
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
|
||||
|
||||
- name: Release
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./dist/*
|
||||
body_path: './release.md'
|
||||
|
||||
- name: Create Fdroid Source Dir
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
mkdir -p ./tmp
|
||||
cp ./dist/*android-arm64-v8a* ./tmp/ || true
|
||||
echo "Files copied successfully"
|
||||
|
||||
- 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
|
||||
env:
|
||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
@@ -238,7 +236,7 @@ jobs:
|
||||
destination-repository-name: FlClash-fdroid-repo
|
||||
user-name: 'github-actions[bot]'
|
||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||
target-branch: action-pr
|
||||
target-branch: main
|
||||
commit-message: Update from ${{ github.ref_name }}
|
||||
target-directory: /tmp/
|
||||
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,14 +1,10 @@
|
||||
[submodule "core/Clash.Meta"]
|
||||
path = core/Clash.Meta
|
||||
url = git@github.com:chen08209/Clash.Meta.git
|
||||
branch = FlClash-Alpha
|
||||
branch = FlClash
|
||||
[submodule "plugins/flutter_distributor"]
|
||||
path = plugins/flutter_distributor
|
||||
url = git@github.com:chen08209/flutter_distributor.git
|
||||
branch = FlClash
|
||||
[submodule "plugins/tray_manager"]
|
||||
path = plugins/tray_manager
|
||||
url = git@github.com:chen08209/tray_manager.git
|
||||
branch = main
|
||||
|
||||
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,3 +1,25 @@
|
||||
## 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
|
||||
|
||||
10
Makefile
Normal file
10
Makefile
Normal 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
|
||||
@@ -1,8 +1 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
|
||||
analyzer:
|
||||
plugins:
|
||||
- custom_lint
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import com.android.build.gradle.tasks.MergeSourceSetFolders
|
||||
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
@@ -33,8 +31,8 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
|
||||
|
||||
android {
|
||||
namespace "com.follow.clash"
|
||||
compileSdkVersion 34
|
||||
ndkVersion "27.1.12297006"
|
||||
compileSdk 35
|
||||
ndkVersion = "28.0.13004108"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -48,6 +46,7 @@ android {
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (isRelease) {
|
||||
release {
|
||||
@@ -63,7 +62,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "com.follow.clash"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
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 {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":core")
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
|
||||
exclude group: "com.google.guava", module: "guava"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
afterEvaluate {
|
||||
assembleDebug.dependsOn copyNativeLibs
|
||||
|
||||
assembleRelease.dependsOn copyNativeLibs
|
||||
}
|
||||
}
|
||||
@@ -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 Flutter tool needs it to communicate with the running application
|
||||
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
|
||||
android:name=".services.FlClashTileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label">
|
||||
</service>
|
||||
android:name=".services.FlClashTileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label"
|
||||
tools:targetApi="24" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -10,14 +10,12 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
@@ -64,7 +62,9 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
|
||||
<activity
|
||||
android:name=".TempActivity"
|
||||
@@ -87,7 +87,6 @@
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:label="FlClash"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
@@ -125,7 +124,7 @@
|
||||
<service
|
||||
android:name=".services.FlClashVpnService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
@@ -138,7 +137,7 @@
|
||||
<service
|
||||
android:name=".services.FlClashService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
android:foregroundServiceType="dataSync">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="service" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.follow.clash;
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context;
|
||||
import android.content.Context
|
||||
|
||||
class FlClashApplication : Application() {
|
||||
companion object {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import io.flutter.FlutterInjector
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.follow.clash
|
||||
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
|
||||
@@ -291,16 +291,18 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
private fun getPackages(): List<Package> {
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||
it.packageName != FlClashApplication.getAppContext().packageName
|
||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
|
||||
?.filter {
|
||||
it.packageName != FlClashApplication.getAppContext().packageName && (
|
||||
it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
)
|
||||
|
||||
}?.map {
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
||||
label = it.applicationInfo?.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
|
||||
lastUpdateTime = it.lastUpdateTime
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
@@ -353,7 +355,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
suspend fun getText(text: String): String? {
|
||||
return withContext(Dispatchers.Default){
|
||||
return withContext(Dispatchers.Default) {
|
||||
channel.awaitResult<String>("getText", text)
|
||||
}
|
||||
}
|
||||
@@ -391,31 +393,33 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}.forEach {
|
||||
if (it.name.matches(chinaAppRegex)) return true
|
||||
}
|
||||
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
}
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
packageInfo.applicationInfo?.publicSourceDir?.let {
|
||||
ZipFile(File(it)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) return true
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.google.gson.Gson
|
||||
@@ -53,7 +52,6 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun handleDestroy() {
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,13 @@ import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.extensions.awaitResult
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.extensions.resolveDns
|
||||
import com.follow.clash.models.Process
|
||||
import com.follow.clash.models.StartForegroundParams
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.follow.clash.services.BaseServiceInterface
|
||||
@@ -41,10 +39,12 @@ import kotlin.concurrent.withLock
|
||||
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
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 var lastStartForegroundParams: StartForegroundParams? = null
|
||||
private var timerJob: Job? = null
|
||||
private val uidPageNameMap = mutableMapOf<Int, String>()
|
||||
|
||||
private val connectivity by lazy {
|
||||
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
|
||||
@@ -52,6 +52,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
isBind = true
|
||||
flClashService = when (service) {
|
||||
is FlClashVpnService.LocalBinder -> service.getService()
|
||||
is FlClashService.LocalBinder -> service.getService()
|
||||
@@ -61,6 +62,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
isBind = false
|
||||
flClashService = null
|
||||
}
|
||||
}
|
||||
@@ -91,62 +93,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
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 -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
@@ -154,6 +100,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
fun handleStart(options: VpnOptions): Boolean {
|
||||
if (options.enable != this.options?.enable) {
|
||||
this.flClashService = null
|
||||
}
|
||||
this.options = options
|
||||
when (options.enable) {
|
||||
true -> handleStartVpn()
|
||||
@@ -163,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()
|
||||
?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestGc() {
|
||||
@@ -236,6 +184,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun startForegroundJob() {
|
||||
stopForegroundJob()
|
||||
timerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (isActive) {
|
||||
startForeground()
|
||||
@@ -257,26 +206,58 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val fd = flClashService?.start(options)
|
||||
flutterMethodChannel.invokeMethod(
|
||||
"started", fd
|
||||
val fd = flClashService?.start(options!!)
|
||||
Core.startTun(
|
||||
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() {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
stopForegroundJob()
|
||||
Core.stopTun()
|
||||
flClashService?.stop()
|
||||
GlobalState.handleTryDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = when (options.enable) {
|
||||
if (isBind) {
|
||||
FlClashApplication.getAppContext().unbindService(connection)
|
||||
}
|
||||
val intent = when (options?.enable == true) {
|
||||
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
|
||||
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
@@ -87,6 +87,7 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
@@ -100,7 +101,8 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
@@ -116,7 +118,11 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
.setContentTitle(title)
|
||||
.setContentText(content).build()
|
||||
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 {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
@@ -45,9 +45,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv4RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}")
|
||||
addRoute(i.address, i.prefixLength)
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute4",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute("0.0.0.0", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute("0.0.0.0", 0)
|
||||
@@ -58,9 +65,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv6RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}")
|
||||
addRoute(i.address, i.prefixLength)
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute6",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute("::", 0)
|
||||
@@ -165,7 +179,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
@@ -182,7 +196,11 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
.setContentText(content)
|
||||
.build()
|
||||
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 {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ subprojects {
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
project.evaluationDependsOn(':core')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
|
||||
1
android/core/.gitignore
vendored
Normal file
1
android/core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
65
android/core/build.gradle.kts
Normal file
65
android/core/build.gradle.kts
Normal 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")
|
||||
}
|
||||
0
android/core/consumer-rules.pro
Normal file
0
android/core/consumer-rules.pro
Normal file
21
android/core/proguard-rules.pro
vendored
Normal file
21
android/core/proguard-rules.pro
vendored
Normal 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
|
||||
4
android/core/src/main/AndroidManifest.xml
Normal file
4
android/core/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
45
android/core/src/main/cpp/CMakeLists.txt
Normal file
45
android/core/src/main/cpp/CMakeLists.txt
Normal 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 ()
|
||||
75
android/core/src/main/cpp/core.cpp
Normal file
75
android/core/src/main/cpp/core.cpp
Normal 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
|
||||
70
android/core/src/main/cpp/jni_helper.cpp
Normal file
70
android/core/src/main/cpp/jni_helper.cpp
Normal 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);
|
||||
}
|
||||
39
android/core/src/main/cpp/jni_helper.h
Normal file
39
android/core/src/main/cpp/jni_helper.h
Normal 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)
|
||||
50
android/core/src/main/java/com/follow/clash/core/Core.kt
Normal file
50
android/core/src/main/java/com/follow/clash/core/Core.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
kotlin_version=1.9.22
|
||||
agp_version=8.2.1
|
||||
agp_version=8.9.1
|
||||
|
||||
@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
||||
|
||||
@@ -24,3 +24,4 @@ plugins {
|
||||
}
|
||||
|
||||
include ":app"
|
||||
include ':core'
|
||||
|
||||
BIN
assets/fonts/JetBrainsMono-Regular.ttf
Normal file
BIN
assets/fonts/JetBrainsMono-Regular.ttf
Normal file
Binary file not shown.
Submodule core/Clash.Meta updated: 76b0d7e8bc...f19dad529f
@@ -35,8 +35,8 @@ func (action Action) getResult(data interface{}) []byte {
|
||||
func handleAction(action *Action, result func(data interface{})) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
data := action.Data.(string)
|
||||
result(handleInitClash(data))
|
||||
paramsString := action.Data.(string)
|
||||
result(handleInitClash(paramsString))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
result(handleGetIsInit())
|
||||
|
||||
77
core/android_bride.go
Normal file
77
core/android_bride.go
Normal 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
|
||||
}
|
||||
@@ -28,8 +28,12 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func splitByComma(s string) interface{} {
|
||||
parts := strings.Split(s, ",")
|
||||
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
|
||||
}
|
||||
@@ -37,6 +41,7 @@ func splitByComma(s string) interface{} {
|
||||
}
|
||||
|
||||
var (
|
||||
version = 0
|
||||
isRunning = false
|
||||
runLock sync.Mutex
|
||||
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
|
||||
@@ -168,18 +173,18 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
|
||||
return prof
|
||||
}
|
||||
|
||||
func genHosts(hosts, patchHosts map[string]any) {
|
||||
func attachHosts(hosts, patchHosts map[string]any) {
|
||||
for k, v := range patchHosts {
|
||||
if str, ok := v.(string); ok {
|
||||
hosts[k] = splitByComma(str)
|
||||
hosts[k] = splitByMultipleSeparators(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modPatchDns(dns *config.RawDNS) {
|
||||
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, splitByComma(str))
|
||||
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,26 +196,25 @@ func trimArr(arr []string) (r []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func overrideRules(rules *[]string) {
|
||||
var target = ""
|
||||
for _, line := range *rules {
|
||||
func overrideRules(rules, patchRules []string) []string {
|
||||
target := ""
|
||||
for _, line := range rules {
|
||||
rule := trimArr(strings.Split(line, ","))
|
||||
l := len(rule)
|
||||
if l != 2 {
|
||||
return
|
||||
if len(rule) != 2 {
|
||||
continue
|
||||
}
|
||||
if strings.ToUpper(rule[0]) == "MATCH" {
|
||||
if strings.EqualFold(rule[0], "MATCH") {
|
||||
target = rule[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == "" {
|
||||
return
|
||||
return rules
|
||||
}
|
||||
var rulesExt = lo.Map(ips, func(ip string, index int) string {
|
||||
return fmt.Sprintf("DOMAIN %s %s", ip, target)
|
||||
rulesExt := lo.Map(ips, func(ip string, _ int) string {
|
||||
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) {
|
||||
@@ -244,16 +248,20 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
for idx := range targetConfig.ProxyGroup {
|
||||
targetConfig.ProxyGroup[idx]["url"] = ""
|
||||
}
|
||||
genHosts(targetConfig.Hosts, patchConfig.Hosts)
|
||||
attachHosts(targetConfig.Hosts, patchConfig.Hosts)
|
||||
if configParams.OverrideDns {
|
||||
modPatchDns(&patchConfig.DNS)
|
||||
updatePatchDns(patchConfig.DNS)
|
||||
targetConfig.DNS = patchConfig.DNS
|
||||
} else {
|
||||
if targetConfig.DNS.Enable == false {
|
||||
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() {
|
||||
|
||||
@@ -7,12 +7,18 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type InitParams struct {
|
||||
HomeDir string `json:"home-dir"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type ConfigExtendedParams struct {
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
OverrideRule bool `json:"override-rule"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
@@ -70,11 +76,7 @@ const (
|
||||
stopLogMethod Method = "stopLog"
|
||||
startListenerMethod Method = "startListener"
|
||||
stopListenerMethod Method = "stopListener"
|
||||
startTunMethod Method = "startTun"
|
||||
stopTunMethod Method = "stopTun"
|
||||
updateDnsMethod Method = "updateDns"
|
||||
setProcessMapMethod Method = "setProcessMap"
|
||||
setFdMapMethod Method = "setFdMap"
|
||||
setStateMethod Method = "setState"
|
||||
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||
getRunTimeMethod Method = "getRunTime"
|
||||
@@ -108,20 +110,3 @@ func (message *Message) Json() (string, error) {
|
||||
data, err := json.Marshal(message)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
type InvokeMessage struct {
|
||||
Type InvokeType `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type InvokeType string
|
||||
|
||||
const (
|
||||
ProtectInvoke InvokeType = "protect"
|
||||
ProcessInvoke InvokeType = "process"
|
||||
)
|
||||
|
||||
func (message *InvokeMessage) Json() string {
|
||||
data, _ := json.Marshal(message)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
11
core/go.mod
11
core/go.mod
@@ -21,7 +21,7 @@ require (
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.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/polyval v0.0.0-20220411101811-e25bc10ba391 // 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/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bart v0.19.0 // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.1 // indirect
|
||||
github.com/metacubex/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/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // 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-wireguard v0.0.0-20241126021510-0827d417b589 // 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/miekg/dns v1.1.63 // 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/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
|
||||
22
core/go.sum
22
core/go.sum
@@ -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/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo=
|
||||
github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw=
|
||||
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
|
||||
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
@@ -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/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
|
||||
github.com/metacubex/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-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
|
||||
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
|
||||
github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI=
|
||||
github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
@@ -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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
|
||||
10
core/hub.go
10
core/hub.go
@@ -34,9 +34,15 @@ var (
|
||||
currentConfig *config.Config
|
||||
)
|
||||
|
||||
func handleInitClash(homeDirStr string) bool {
|
||||
func handleInitClash(paramsString string) bool {
|
||||
var params = InitParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
version = params.Version
|
||||
if !isInit {
|
||||
constant.SetHomeDir(homeDirStr)
|
||||
constant.SetHomeDir(params.HomeDir)
|
||||
isInit = true
|
||||
}
|
||||
return isInit
|
||||
|
||||
@@ -4,6 +4,7 @@ package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
bridge "core/dart-bridge"
|
||||
"core/platform"
|
||||
"core/state"
|
||||
@@ -11,121 +12,116 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/process"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/dns"
|
||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Fd struct {
|
||||
Id string `json:"id"`
|
||||
Value int64 `json:"value"`
|
||||
type TunHandler struct {
|
||||
listener *sing_tun.Listener
|
||||
callback unsafe.Pointer
|
||||
|
||||
limit *semaphore.Weighted
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
Id string `json:"id"`
|
||||
Metadata *constant.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type ProcessMapItem struct {
|
||||
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{}),
|
||||
func (t *TunHandler) close() {
|
||||
_ = t.limit.Acquire(context.TODO(), 4)
|
||||
defer t.limit.Release(4)
|
||||
removeTunHook()
|
||||
if t.listener != nil {
|
||||
_ = t.listener.Close()
|
||||
}
|
||||
|
||||
if t.callback != nil {
|
||||
releaseObject(t.callback)
|
||||
}
|
||||
t.callback = nil
|
||||
t.listener = nil
|
||||
}
|
||||
|
||||
func (m *InvokeManager) completer(id string, value string) {
|
||||
m.invokeMap.Store(id, value)
|
||||
m.chanLock.Lock()
|
||||
if ch, ok := m.chanMap[id]; ok {
|
||||
close(ch)
|
||||
delete(m.chanMap, id)
|
||||
func (t *TunHandler) handleProtect(fd int) {
|
||||
_ = t.limit.Acquire(context.Background(), 1)
|
||||
defer t.limit.Release(1)
|
||||
|
||||
if t.listener == nil {
|
||||
return
|
||||
}
|
||||
m.chanLock.Unlock()
|
||||
|
||||
protect(t.callback, fd)
|
||||
}
|
||||
|
||||
func (m *InvokeManager) await(id string) string {
|
||||
m.chanLock.Lock()
|
||||
if _, ok := m.chanMap[id]; !ok {
|
||||
m.chanMap[id] = make(chan struct{})
|
||||
}
|
||||
ch := m.chanMap[id]
|
||||
m.chanLock.Unlock()
|
||||
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
|
||||
_ = t.limit.Acquire(context.Background(), 1)
|
||||
defer t.limit.Release(1)
|
||||
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
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, "")
|
||||
if t.listener == nil {
|
||||
return ""
|
||||
}
|
||||
var protocol int
|
||||
uid := -1
|
||||
switch source.Network() {
|
||||
case "udp", "udp4", "udp6":
|
||||
protocol = syscall.IPPROTO_UDP
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
protocol = syscall.IPPROTO_TCP
|
||||
}
|
||||
if version < 29 {
|
||||
uid = platform.QuerySocketUidFromProcFs(source, target)
|
||||
}
|
||||
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
|
||||
}
|
||||
|
||||
var (
|
||||
invokePort int64 = -1
|
||||
tunListener *sing_tun.Listener
|
||||
fdInvokeMap = NewInvokeManager()
|
||||
processInvokeMap = NewInvokeManager()
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
tunHandler *TunHandler
|
||||
)
|
||||
|
||||
func handleStartTun(fd int) string {
|
||||
handleStopTun()
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
if fd == 0 {
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
} else {
|
||||
initSocketHook()
|
||||
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
}
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
}
|
||||
return handleGetRunTime()
|
||||
}
|
||||
|
||||
func handleStopTun() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
removeSocketHook()
|
||||
runTime = nil
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN close")
|
||||
_ = tunListener.Close()
|
||||
if tunHandler != nil {
|
||||
tunHandler.close()
|
||||
}
|
||||
}
|
||||
|
||||
func handleStartTun(fd int, callback unsafe.Pointer) bool {
|
||||
handleStopTun()
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
if fd != 0 {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
tunHandler = &TunHandler{
|
||||
callback: callback,
|
||||
limit: semaphore.NewWeighted(4),
|
||||
}
|
||||
initTunHook()
|
||||
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
} else {
|
||||
removeTunHook()
|
||||
return false
|
||||
}
|
||||
tunHandler.listener = tunListener
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleGetRunTime() string {
|
||||
if runTime == nil {
|
||||
return ""
|
||||
@@ -133,83 +129,29 @@ func handleGetRunTime() string {
|
||||
return strconv.FormatInt(runTime.UnixMilli(), 10)
|
||||
}
|
||||
|
||||
func handleSetProcessMap(params string) {
|
||||
var processMapItem = &ProcessMapItem{}
|
||||
err := json.Unmarshal([]byte(params), processMapItem)
|
||||
if err == nil {
|
||||
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
|
||||
}
|
||||
}
|
||||
|
||||
//export attachInvokePort
|
||||
func attachInvokePort(mPort C.longlong) {
|
||||
invokePort = int64(mPort)
|
||||
}
|
||||
|
||||
func sendInvokeMessage(message InvokeMessage) {
|
||||
if invokePort == -1 {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(invokePort, message.Json())
|
||||
}
|
||||
|
||||
func handleMarkSocket(fd Fd) {
|
||||
sendInvokeMessage(InvokeMessage{
|
||||
Type: ProtectInvoke,
|
||||
Data: fd,
|
||||
})
|
||||
}
|
||||
|
||||
func handleParseProcess(process Process) {
|
||||
sendInvokeMessage(InvokeMessage{
|
||||
Type: ProcessInvoke,
|
||||
Data: process,
|
||||
})
|
||||
}
|
||||
|
||||
func handleSetFdMap(id string) {
|
||||
go func() {
|
||||
fdInvokeMap.completer(id, "")
|
||||
}()
|
||||
}
|
||||
|
||||
func initSocketHook() {
|
||||
func initTunHook() {
|
||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||
if platform.ShouldBlockConnection() {
|
||||
return errBlocked
|
||||
}
|
||||
return conn.Control(func(fd uintptr) {
|
||||
fdInt := int64(fd)
|
||||
id := utils.NewUUIDV1().String()
|
||||
|
||||
handleMarkSocket(Fd{
|
||||
Id: id,
|
||||
Value: fdInt,
|
||||
})
|
||||
|
||||
fdInvokeMap.await(id)
|
||||
tunHandler.handleProtect(int(fd))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func removeSocketHook() {
|
||||
dialer.DefaultSocketHook = nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
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
|
||||
}
|
||||
id := utils.NewUUIDV1().String()
|
||||
handleParseProcess(Process{
|
||||
Id: id,
|
||||
Metadata: metadata,
|
||||
})
|
||||
return processInvokeMap.await(id), nil
|
||||
return tunHandler.handleResolveProcess(src, dst), nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeTunHook() {
|
||||
dialer.DefaultSocketHook = nil
|
||||
process.DefaultPackageNameResolver = nil
|
||||
}
|
||||
|
||||
func handleGetAndroidVpnOptions() string {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
@@ -250,16 +192,6 @@ func handleGetCurrentProfileName() string {
|
||||
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
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:
|
||||
result(handleGetAndroidVpnOptions())
|
||||
return true
|
||||
@@ -268,16 +200,6 @@ func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
handleUpdateDns(data)
|
||||
result(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:
|
||||
result(handleGetRunTime())
|
||||
return true
|
||||
@@ -289,13 +211,13 @@ func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
}
|
||||
|
||||
//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)
|
||||
dir := C.GoString(dirChar)
|
||||
paramsString := C.GoString(initParamsChar)
|
||||
bytes := []byte(C.GoString(paramsChar))
|
||||
stateParams := C.GoString(stateParamsChar)
|
||||
go func() {
|
||||
res := handleInitClash(dir)
|
||||
res := handleInitClash(paramsString)
|
||||
if res == false {
|
||||
bridge.SendToPort(i, "init error")
|
||||
}
|
||||
@@ -305,9 +227,8 @@ func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, po
|
||||
}
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int) *C.char {
|
||||
f := int(fd)
|
||||
return C.CString(handleStartTun(f))
|
||||
func startTUN(fd C.int, callback unsafe.Pointer) bool {
|
||||
return handleStartTun(int(fd), callback)
|
||||
}
|
||||
|
||||
//export getRunTime
|
||||
@@ -320,12 +241,6 @@ func stopTun() {
|
||||
handleStopTun()
|
||||
}
|
||||
|
||||
//export setFdMap
|
||||
func setFdMap(fdIdChar *C.char) {
|
||||
fdId := C.GoString(fdIdChar)
|
||||
handleSetFdMap(fdId)
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
return C.CString(handleGetCurrentProfileName())
|
||||
@@ -347,12 +262,3 @@ func updateDns(s *C.char) {
|
||||
dnsList := C.GoString(s)
|
||||
handleUpdateDns(dnsList)
|
||||
}
|
||||
|
||||
//export setProcessMap
|
||||
func setProcessMap(s *C.char) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
paramsString := C.GoString(s)
|
||||
handleSetProcessMap(paramsString)
|
||||
}
|
||||
|
||||
176
core/platform/procfs.go
Normal file
176
core/platform/procfs.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/manager/hotkey_manager.dart';
|
||||
import 'package:fl_clash/manager/manager.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@@ -43,16 +43,8 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
ColorScheme _getAppColorScheme({
|
||||
required Brightness brightness,
|
||||
int? primaryColor,
|
||||
required ColorSchemes systemColorSchemes,
|
||||
}) {
|
||||
if (primaryColor != null) {
|
||||
return ColorScheme.fromSeed(
|
||||
seedColor: Color(primaryColor),
|
||||
brightness: brightness,
|
||||
);
|
||||
} else {
|
||||
return systemColorSchemes.getColorSchemeForBrightness(brightness);
|
||||
}
|
||||
return ref.read(genColorSchemeProvider(brightness));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -61,16 +53,6 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
_autoUpdateGroupTask();
|
||||
_autoUpdateProfilesTask();
|
||||
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 {
|
||||
final currentContext = globalState.navigatorKey.currentContext;
|
||||
if (currentContext != null) {
|
||||
@@ -98,7 +80,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_buildPlatformWrap(Widget child) {
|
||||
_buildPlatformState(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowManager(
|
||||
child: TrayManager(
|
||||
@@ -117,18 +99,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_buildPage(Widget page) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(
|
||||
child: page,
|
||||
);
|
||||
}
|
||||
return VpnManager(
|
||||
child: page,
|
||||
);
|
||||
}
|
||||
|
||||
_buildWrap(Widget child) {
|
||||
_buildState(Widget child) {
|
||||
return AppStateManager(
|
||||
child: ClashManager(
|
||||
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(
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
@@ -157,8 +147,8 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildPlatformWrap(
|
||||
_buildWrap(
|
||||
return _buildPlatformState(
|
||||
_buildState(
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final locale =
|
||||
@@ -168,6 +158,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
@@ -176,20 +167,15 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return MessageManager(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
globalState.appController.updateViewWidth(
|
||||
container.maxWidth,
|
||||
);
|
||||
return _buildPage(child!);
|
||||
},
|
||||
return AppEnvManager(
|
||||
child: _buildPlatformApp(
|
||||
_buildApp(child!),
|
||||
),
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(locale),
|
||||
locale: utils.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
@@ -197,7 +183,6 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
),
|
||||
),
|
||||
@@ -206,7 +191,6 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPureBlack(themeProps.pureBlack),
|
||||
),
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
@@ -66,7 +67,12 @@ class ClashCore {
|
||||
Future<bool> init() async {
|
||||
await initGeo();
|
||||
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 {
|
||||
@@ -240,7 +246,7 @@ class ClashCore {
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return ClashConfigSnippet.fromJson(json.decode(res));
|
||||
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
|
||||
}
|
||||
|
||||
resetTraffic() {
|
||||
|
||||
@@ -2348,6 +2348,97 @@ class ClashFFI {
|
||||
|
||||
set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value;
|
||||
|
||||
void protect(
|
||||
protect_func fn,
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
int fd,
|
||||
) {
|
||||
return _protect(
|
||||
fn,
|
||||
tun_interface,
|
||||
fd,
|
||||
);
|
||||
}
|
||||
|
||||
late final _protectPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
protect_func, ffi.Pointer<ffi.Void>, ffi.Int)>>('protect');
|
||||
late final _protect = _protectPtr
|
||||
.asFunction<void Function(protect_func, ffi.Pointer<ffi.Void>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> resolve_process(
|
||||
resolve_process_func fn,
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
int protocol,
|
||||
ffi.Pointer<ffi.Char> source,
|
||||
ffi.Pointer<ffi.Char> target,
|
||||
int uid,
|
||||
) {
|
||||
return _resolve_process(
|
||||
fn,
|
||||
tun_interface,
|
||||
protocol,
|
||||
source,
|
||||
target,
|
||||
uid,
|
||||
);
|
||||
}
|
||||
|
||||
late final _resolve_processPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.Char> Function(
|
||||
resolve_process_func,
|
||||
ffi.Pointer<ffi.Void>,
|
||||
ffi.Int,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Int)>>('resolve_process');
|
||||
late final _resolve_process = _resolve_processPtr.asFunction<
|
||||
ffi.Pointer<ffi.Char> Function(
|
||||
resolve_process_func,
|
||||
ffi.Pointer<ffi.Void>,
|
||||
int,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
int)>();
|
||||
|
||||
void release_object(
|
||||
release_object_func fn,
|
||||
ffi.Pointer<ffi.Void> obj,
|
||||
) {
|
||||
return _release_object(
|
||||
fn,
|
||||
obj,
|
||||
);
|
||||
}
|
||||
|
||||
late final _release_objectPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
release_object_func, ffi.Pointer<ffi.Void>)>>('release_object');
|
||||
late final _release_object = _release_objectPtr
|
||||
.asFunction<void Function(release_object_func, ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void registerCallbacks(
|
||||
protect_func markSocketFunc,
|
||||
resolve_process_func resolveProcessFunc,
|
||||
release_object_func releaseObjectFunc,
|
||||
) {
|
||||
return _registerCallbacks(
|
||||
markSocketFunc,
|
||||
resolveProcessFunc,
|
||||
releaseObjectFunc,
|
||||
);
|
||||
}
|
||||
|
||||
late final _registerCallbacksPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(protect_func, resolve_process_func,
|
||||
release_object_func)>>('registerCallbacks');
|
||||
late final _registerCallbacks = _registerCallbacksPtr.asFunction<
|
||||
void Function(protect_func, resolve_process_func, release_object_func)>();
|
||||
|
||||
void initNativeApiBridge(
|
||||
ffi.Pointer<ffi.Void> api,
|
||||
) {
|
||||
@@ -2443,28 +2534,14 @@ class ClashFFI {
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
|
||||
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
|
||||
|
||||
void attachInvokePort(
|
||||
int mPort,
|
||||
) {
|
||||
return _attachInvokePort(
|
||||
mPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _attachInvokePortPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'attachInvokePort');
|
||||
late final _attachInvokePort =
|
||||
_attachInvokePortPtr.asFunction<void Function(int)>();
|
||||
|
||||
void quickStart(
|
||||
ffi.Pointer<ffi.Char> dirChar,
|
||||
ffi.Pointer<ffi.Char> initParamsChar,
|
||||
ffi.Pointer<ffi.Char> paramsChar,
|
||||
ffi.Pointer<ffi.Char> stateParamsChar,
|
||||
int port,
|
||||
) {
|
||||
return _quickStart(
|
||||
dirChar,
|
||||
initParamsChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
port,
|
||||
@@ -2479,19 +2556,21 @@ class ClashFFI {
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> startTUN(
|
||||
int startTUN(
|
||||
int fd,
|
||||
ffi.Pointer<ffi.Void> callback,
|
||||
) {
|
||||
return _startTUN(
|
||||
fd,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
late final _startTUNPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'startTUN');
|
||||
late final _startTUNPtr = _lookup<
|
||||
ffi.NativeFunction<GoUint8 Function(ffi.Int, ffi.Pointer<ffi.Void>)>>(
|
||||
'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() {
|
||||
return _getRunTime();
|
||||
@@ -2511,20 +2590,6 @@ class ClashFFI {
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
|
||||
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
|
||||
|
||||
void setFdMap(
|
||||
ffi.Pointer<ffi.Char> fdIdChar,
|
||||
) {
|
||||
return _setFdMap(
|
||||
fdIdChar,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setFdMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setFdMap');
|
||||
late final _setFdMap =
|
||||
_setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getCurrentProfileName() {
|
||||
return _getCurrentProfileName();
|
||||
}
|
||||
@@ -2572,20 +2637,6 @@ class ClashFFI {
|
||||
'updateDns');
|
||||
late final _updateDns =
|
||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void setProcessMap(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _setProcessMap(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setProcessMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setProcessMap');
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
}
|
||||
|
||||
final class __mbstate_t extends ffi.Union {
|
||||
@@ -3738,6 +3789,31 @@ typedef mode_t = __darwin_mode_t;
|
||||
typedef __darwin_mode_t = __uint16_t;
|
||||
typedef __uint16_t = ffi.UnsignedShort;
|
||||
typedef Dart__uint16_t = int;
|
||||
typedef protect_func = ffi.Pointer<ffi.NativeFunction<protect_funcFunction>>;
|
||||
typedef protect_funcFunction = ffi.Void Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface, ffi.Int fd);
|
||||
typedef Dartprotect_funcFunction = void Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface, int fd);
|
||||
typedef resolve_process_func
|
||||
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
|
||||
typedef resolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
ffi.Int protocol,
|
||||
ffi.Pointer<ffi.Char> source,
|
||||
ffi.Pointer<ffi.Char> target,
|
||||
ffi.Int uid);
|
||||
typedef Dartresolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
int protocol,
|
||||
ffi.Pointer<ffi.Char> source,
|
||||
ffi.Pointer<ffi.Char> target,
|
||||
int uid);
|
||||
typedef release_object_func
|
||||
= ffi.Pointer<ffi.NativeFunction<release_object_funcFunction>>;
|
||||
typedef release_object_funcFunction = ffi.Void Function(
|
||||
ffi.Pointer<ffi.Void> obj);
|
||||
typedef Dartrelease_object_funcFunction = void Function(
|
||||
ffi.Pointer<ffi.Void> obj);
|
||||
|
||||
final class GoInterface extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.Void> t;
|
||||
@@ -3758,6 +3834,8 @@ final class GoSlice extends ffi.Struct {
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
typedef DartGoInt64 = int;
|
||||
typedef GoUint8 = ffi.UnsignedChar;
|
||||
typedef DartGoUint8 = int;
|
||||
|
||||
const int __has_safe_buffers = 1;
|
||||
|
||||
@@ -3973,6 +4051,8 @@ const int __MAC_15_0 = 150000;
|
||||
|
||||
const int __MAC_15_1 = 150100;
|
||||
|
||||
const int __MAC_15_2 = 150200;
|
||||
|
||||
const int __IPHONE_2_0 = 20000;
|
||||
|
||||
const int __IPHONE_2_1 = 20100;
|
||||
@@ -4135,6 +4215,8 @@ const int __IPHONE_18_0 = 180000;
|
||||
|
||||
const int __IPHONE_18_1 = 180100;
|
||||
|
||||
const int __IPHONE_18_2 = 180200;
|
||||
|
||||
const int __WATCHOS_1_0 = 10000;
|
||||
|
||||
const int __WATCHOS_2_0 = 20000;
|
||||
@@ -4233,6 +4315,8 @@ const int __WATCHOS_11_0 = 110000;
|
||||
|
||||
const int __WATCHOS_11_1 = 110100;
|
||||
|
||||
const int __WATCHOS_11_2 = 110200;
|
||||
|
||||
const int __TVOS_9_0 = 90000;
|
||||
|
||||
const int __TVOS_9_1 = 90100;
|
||||
@@ -4333,6 +4417,8 @@ const int __TVOS_18_0 = 180000;
|
||||
|
||||
const int __TVOS_18_1 = 180100;
|
||||
|
||||
const int __TVOS_18_2 = 180200;
|
||||
|
||||
const int __BRIDGEOS_2_0 = 20000;
|
||||
|
||||
const int __BRIDGEOS_3_0 = 30000;
|
||||
@@ -4389,6 +4475,8 @@ const int __BRIDGEOS_9_0 = 90000;
|
||||
|
||||
const int __BRIDGEOS_9_1 = 90100;
|
||||
|
||||
const int __BRIDGEOS_9_2 = 90200;
|
||||
|
||||
const int __DRIVERKIT_19_0 = 190000;
|
||||
|
||||
const int __DRIVERKIT_20_0 = 200000;
|
||||
@@ -4419,6 +4507,8 @@ const int __DRIVERKIT_24_0 = 240000;
|
||||
|
||||
const int __DRIVERKIT_24_1 = 240100;
|
||||
|
||||
const int __DRIVERKIT_24_2 = 240200;
|
||||
|
||||
const int __VISIONOS_1_0 = 10000;
|
||||
|
||||
const int __VISIONOS_1_1 = 10100;
|
||||
@@ -4429,6 +4519,8 @@ const int __VISIONOS_2_0 = 20000;
|
||||
|
||||
const int __VISIONOS_2_1 = 20100;
|
||||
|
||||
const int __VISIONOS_2_2 = 20200;
|
||||
|
||||
const int MAC_OS_X_VERSION_10_0 = 1000;
|
||||
|
||||
const int MAC_OS_X_VERSION_10_1 = 1010;
|
||||
@@ -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_2 = 150200;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
|
||||
mixin ClashInterface {
|
||||
Future<bool> init(String homeDir);
|
||||
Future<bool> init(InitParams params);
|
||||
|
||||
Future<bool> preload();
|
||||
|
||||
@@ -74,12 +74,10 @@ mixin AndroidClashInterface {
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
Future<bool> stopTun();
|
||||
// Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
|
||||
Future<DateTime?> startTun(int fd);
|
||||
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||
|
||||
Future<String> getCurrentProfileName();
|
||||
@@ -153,7 +151,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) async {
|
||||
final id = "${method.name}#${other.id}";
|
||||
final id = "${method.name}#${utils.id}";
|
||||
|
||||
callbackCompleterMap[id] = Completer<T>();
|
||||
|
||||
@@ -191,10 +189,10 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> init(String homeDir) {
|
||||
Future<bool> init(InitParams params) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.initClash,
|
||||
data: homeDir,
|
||||
data: json.encode(params),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -122,25 +122,12 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> startTun(int fd) async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.startTun,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopTun() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopTun,
|
||||
);
|
||||
}
|
||||
// @override
|
||||
// Future<bool> stopTun() {
|
||||
// return invoke<bool>(
|
||||
// method: ActionMethod.stopTun,
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
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) {
|
||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateDns(dnsChar);
|
||||
malloc.free(dnsChar);
|
||||
}
|
||||
|
||||
setProcessMap(ProcessMapItem processMapItem) {
|
||||
final processMapItemChar =
|
||||
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setProcessMap(processMapItemChar);
|
||||
malloc.free(processMapItemChar);
|
||||
}
|
||||
|
||||
setState(CoreState state) {
|
||||
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setState(stateChar);
|
||||
@@ -305,17 +267,11 @@ class ClashLibHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
setFdMap(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.setFdMap(idChar);
|
||||
malloc.free(idChar);
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
String homeDir,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
InitParams initParams,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
@@ -325,17 +281,18 @@ class ClashLibHandler {
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final initValue = json.encode(initParams);
|
||||
final stateParams = json.encode(state);
|
||||
final homeChar = homeDir.toNativeUtf8().cast<Char>();
|
||||
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.quickStart(
|
||||
homeChar,
|
||||
initParamsChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(homeChar);
|
||||
malloc.free(initParamsChar);
|
||||
malloc.free(paramsChar);
|
||||
malloc.free(stateParamsChar);
|
||||
return completer.future;
|
||||
|
||||
@@ -1,27 +1,92 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ColorExtension on Color {
|
||||
Color get toLight {
|
||||
return withOpacity(0.8);
|
||||
Color get opacity80 {
|
||||
return withAlpha(204);
|
||||
}
|
||||
|
||||
Color get toLighter {
|
||||
return withOpacity(0.6);
|
||||
Color get opacity60 {
|
||||
return withAlpha(153);
|
||||
}
|
||||
|
||||
Color get toSoft {
|
||||
return withOpacity(0.15);
|
||||
Color get opacity50 {
|
||||
return withAlpha(128);
|
||||
}
|
||||
|
||||
Color get toLittle {
|
||||
return withOpacity(0.03);
|
||||
Color get opacity38 {
|
||||
return withAlpha(97);
|
||||
}
|
||||
|
||||
Color darken([double amount = .1]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
final hsl = HSLColor.fromColor(this);
|
||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
return hslDark.toColor();
|
||||
Color get opacity30 {
|
||||
return withAlpha(77);
|
||||
}
|
||||
|
||||
Color get opacity15 {
|
||||
return withAlpha(38);
|
||||
}
|
||||
|
||||
Color get opacity10 {
|
||||
return withAlpha(15);
|
||||
}
|
||||
|
||||
Color get opacity3 {
|
||||
return withAlpha(76);
|
||||
}
|
||||
|
||||
Color get opacity0 {
|
||||
return withAlpha(0);
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -54,7 +119,7 @@ extension ColorSchemeExtension on ColorScheme {
|
||||
? copyWith(
|
||||
surface: Colors.black,
|
||||
surfaceContainer: surfaceContainer.darken(
|
||||
0.05,
|
||||
5,
|
||||
),
|
||||
)
|
||||
: this;
|
||||
|
||||
@@ -19,7 +19,7 @@ export 'navigation.dart';
|
||||
export 'navigator.dart';
|
||||
export 'network.dart';
|
||||
export 'num.dart';
|
||||
export 'other.dart';
|
||||
export 'utils.dart';
|
||||
export 'package.dart';
|
||||
export 'path.dart';
|
||||
export 'picker.dart';
|
||||
|
||||
@@ -21,6 +21,11 @@ const baseInfoEdgeInsets = EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
);
|
||||
|
||||
double textScaleFactor = min(
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||
1.2,
|
||||
);
|
||||
const httpTimeoutDuration = Duration(milliseconds: 5000);
|
||||
const moreDuration = Duration(milliseconds: 100);
|
||||
const animateDuration = Duration(milliseconds: 100);
|
||||
@@ -46,7 +51,7 @@ const defaultExternalController = "127.0.0.1:9090";
|
||||
const maxMobileWidth = 600;
|
||||
const maxLaptopWidth = 840;
|
||||
const defaultTestUrl = "https://www.gstatic.com/generate_204";
|
||||
final filter = ImageFilter.blur(
|
||||
final commonFilter = ImageFilter.blur(
|
||||
sigmaX: 5,
|
||||
sigmaY: 5,
|
||||
tileMode: TileMode.mirror,
|
||||
@@ -73,12 +78,22 @@ const viewModeColumnsMap = {
|
||||
ViewMode.desktop: [4, 3],
|
||||
};
|
||||
|
||||
const defaultPrimaryColor = Colors.brown;
|
||||
const defaultPrimaryColor = 0xFF795548;
|
||||
|
||||
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 serviceIsolate = "FlClashServiceIsolate";
|
||||
|
||||
const defaultPrimaryColors = [
|
||||
defaultPrimaryColor,
|
||||
0xFF03A9F4,
|
||||
0xFFFFFF00,
|
||||
0XFFBBC9CC,
|
||||
0XFFABD397,
|
||||
0XFFD8C0C3,
|
||||
0XFF665390,
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
class Debouncer {
|
||||
final Map<dynamic, Timer> _operations = {};
|
||||
final Map<dynamic, Timer?> _operations = {};
|
||||
|
||||
call(
|
||||
dynamic tag,
|
||||
@@ -28,14 +28,15 @@ class Debouncer {
|
||||
|
||||
cancel(dynamic tag) {
|
||||
_operations[tag]?.cancel();
|
||||
_operations[tag] = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Throttler {
|
||||
final Map<dynamic, Timer> _operations = {};
|
||||
final Map<dynamic, Timer?> _operations = {};
|
||||
|
||||
call(
|
||||
String tag,
|
||||
dynamic tag,
|
||||
Function func, {
|
||||
List<dynamic>? args,
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
@@ -60,6 +61,7 @@ class Throttler {
|
||||
|
||||
cancel(dynamic tag) {
|
||||
_operations[tag]?.cancel();
|
||||
_operations[tag] = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,3 +65,12 @@ extension DoubleListExt on List<double> {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
extension MapExt<K, V> on Map<K, V> {
|
||||
getCacheValue(K key, V defaultValue) {
|
||||
if (this[key] == null) {
|
||||
this[key] = defaultValue;
|
||||
}
|
||||
return this[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
|
||||
import 'constant.dart';
|
||||
@@ -34,6 +35,9 @@ class AutoLaunch {
|
||||
}
|
||||
|
||||
updateStatus(bool isAutoLaunch) async {
|
||||
if(kDebugMode){
|
||||
return;
|
||||
}
|
||||
if (await isEnable == isAutoLaunch) return;
|
||||
if (isAutoLaunch == true) {
|
||||
enable();
|
||||
|
||||
@@ -32,7 +32,7 @@ class FixedList<T> {
|
||||
}
|
||||
|
||||
class FixedMap<K, V> {
|
||||
final int maxSize;
|
||||
int maxSize;
|
||||
final Map<K, V> _map = {};
|
||||
final Queue<K> _queue = Queue<K>();
|
||||
|
||||
@@ -45,6 +45,7 @@ class FixedMap<K, V> {
|
||||
}
|
||||
_map[key] = value;
|
||||
_queue.add(key);
|
||||
return value;
|
||||
}
|
||||
|
||||
clear() {
|
||||
@@ -52,8 +53,13 @@ class FixedMap<K, V> {
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
updateMaxSize(int size){
|
||||
maxSize = size;
|
||||
}
|
||||
|
||||
V? get(K key) => _map[key];
|
||||
|
||||
|
||||
bool containsKey(K key) => _map.containsKey(key);
|
||||
|
||||
int get length => _map.length;
|
||||
|
||||
@@ -5,13 +5,11 @@ import 'package:flutter/material.dart';
|
||||
class Measure {
|
||||
final TextScaler _textScale;
|
||||
final BuildContext context;
|
||||
final String? _fontFamily;
|
||||
|
||||
Measure.of(this.context, {String? fontFamily})
|
||||
Measure.of(this.context)
|
||||
: _textScale = TextScaler.linear(
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||
),
|
||||
_fontFamily = fontFamily ?? "";
|
||||
textScaleFactor,
|
||||
);
|
||||
|
||||
Size computeTextSize(
|
||||
Text text, {
|
||||
@@ -20,9 +18,7 @@ class Measure {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text.data,
|
||||
style: text.style?.copyWith(
|
||||
fontFamily: _fontFamily,
|
||||
),
|
||||
style: text.style,
|
||||
),
|
||||
maxLines: text.maxLines,
|
||||
textScaler: _textScale,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'context.dart';
|
||||
@@ -29,8 +30,14 @@ mixin PageMixin<T extends StatefulWidget> on State<T> {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.actions = actions;
|
||||
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||
commonScaffoldState?.onSearch = onSearch;
|
||||
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||
commonScaffoldState?.updateSearchState(
|
||||
(_) => onSearch != null
|
||||
? AppBarSearchState(
|
||||
onSearch: onSearch!,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
|
||||
Duration get transitionDuration => const Duration(milliseconds: 500);
|
||||
|
||||
@override
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 250);
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 500);
|
||||
}
|
||||
|
||||
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
|
||||
@@ -194,7 +194,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
|
||||
_primaryPositionCurve = CurvedAnimation(
|
||||
parent: widget.primaryRouteAnimation,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
reverseCurve: Curves.easeInOut,
|
||||
reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped,
|
||||
);
|
||||
_secondaryPositionCurve = CurvedAnimation(
|
||||
parent: widget.secondaryRouteAnimation,
|
||||
@@ -218,9 +218,8 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
|
||||
begin: const _CommonEdgeShadowDecoration(),
|
||||
end: _CommonEdgeShadowDecoration(
|
||||
<Color>[
|
||||
widget.context.colorScheme.inverseSurface.withOpacity(
|
||||
0.06,
|
||||
),
|
||||
widget.context.colorScheme.inverseSurface
|
||||
.withValues(alpha: 0.02),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
@@ -274,7 +273,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
|
||||
return;
|
||||
}
|
||||
|
||||
final double shadowWidth = 0.03 * configuration.size!.width;
|
||||
final double shadowWidth = 1 * configuration.size!.width;
|
||||
final double shadowHeight = configuration.size!.height;
|
||||
final double bandWidth = shadowWidth / (colors.length - 1);
|
||||
|
||||
|
||||
@@ -14,15 +14,13 @@ class Protocol {
|
||||
|
||||
void register(String scheme) {
|
||||
String protocolRegKey = 'Software\\Classes\\$scheme';
|
||||
RegistryValue protocolRegValue = const RegistryValue(
|
||||
RegistryValue protocolRegValue = RegistryValue.string(
|
||||
'URL Protocol',
|
||||
RegistryValueType.string,
|
||||
'',
|
||||
);
|
||||
String protocolCmdRegKey = 'shell\\open\\command';
|
||||
RegistryValue protocolCmdRegValue = RegistryValue(
|
||||
RegistryValue protocolCmdRegValue = RegistryValue.string(
|
||||
'',
|
||||
RegistryValueType.string,
|
||||
'"${Platform.resolvedExecutable}" "%1"',
|
||||
);
|
||||
final regKey = Registry.currentUser.createKey(protocolRegKey);
|
||||
@@ -31,4 +29,4 @@ class Protocol {
|
||||
}
|
||||
}
|
||||
|
||||
final protocol = Protocol();
|
||||
final protocol = Protocol();
|
||||
|
||||
@@ -22,7 +22,7 @@ class Render {
|
||||
}
|
||||
|
||||
pause() {
|
||||
debouncer.call(
|
||||
throttler.call(
|
||||
DebounceTag.renderPause,
|
||||
_pause,
|
||||
duration: Duration(seconds: 5),
|
||||
@@ -30,11 +30,11 @@ class Render {
|
||||
}
|
||||
|
||||
resume() {
|
||||
debouncer.cancel(DebounceTag.renderPause);
|
||||
throttler.cancel(DebounceTag.renderPause);
|
||||
_resume();
|
||||
}
|
||||
|
||||
void _pause() {
|
||||
void _pause() async {
|
||||
if (_isPaused) return;
|
||||
_isPaused = true;
|
||||
_beginFrame = _dispatcher.onBeginFrame;
|
||||
@@ -54,4 +54,4 @@ class Render {
|
||||
}
|
||||
}
|
||||
|
||||
final render = system.isDesktop ? Render() : null;
|
||||
final Render? render = system.isDesktop ? Render() : null;
|
||||
|
||||
@@ -68,7 +68,7 @@ class Request {
|
||||
final remoteVersion = data['tag_name'];
|
||||
final version = globalState.packageInfo.version;
|
||||
final hasUpdate =
|
||||
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
|
||||
utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
|
||||
if (!hasUpdate) return null;
|
||||
return data;
|
||||
}
|
||||
@@ -83,13 +83,19 @@ class Request {
|
||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||
for (final source in _ipInfoSources.entries) {
|
||||
try {
|
||||
final response = await _dio.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
final response = await Dio()
|
||||
.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
)
|
||||
.timeout(
|
||||
Duration(
|
||||
seconds: 30,
|
||||
),
|
||||
);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/widgets/scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseScrollBehavior extends MaterialScrollBehavior {
|
||||
@@ -16,8 +17,6 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
|
||||
};
|
||||
}
|
||||
|
||||
class BaseScrollBehavior2 extends ScrollBehavior {}
|
||||
|
||||
class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
||||
@override
|
||||
Widget buildScrollbar(
|
||||
@@ -36,8 +35,7 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) {
|
||||
return Scrollbar(
|
||||
interactive: true,
|
||||
return CommonAutoHiddenScrollBar(
|
||||
controller: details.controller,
|
||||
child: child,
|
||||
);
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'color.dart';
|
||||
|
||||
extension TextStyleExtension on TextStyle {
|
||||
TextStyle get toLight => copyWith(color: color?.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 toBold => copyWith(fontWeight: FontWeight.bold);
|
||||
|
||||
TextStyle get toJetBrainsMono => copyWith(
|
||||
fontFamily: FontFamily.jetBrainsMono.value,
|
||||
);
|
||||
|
||||
TextStyle adjustSize(int size) => copyWith(
|
||||
fontSize: fontSize! + size,
|
||||
);
|
||||
|
||||
39
lib/common/theme.dart
Normal file
39
lib/common/theme.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/utils.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -10,7 +11,6 @@ import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
import 'app_localizations.dart';
|
||||
import 'constant.dart';
|
||||
import 'other.dart';
|
||||
import 'window.dart';
|
||||
|
||||
class Tray {
|
||||
@@ -25,7 +25,7 @@ class Tray {
|
||||
await trayManager.destroy();
|
||||
}
|
||||
await trayManager.setIcon(
|
||||
other.getTrayIconPath(
|
||||
utils.getTrayIconPath(
|
||||
brightness: brightness ??
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
),
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
|
||||
class Other {
|
||||
class Utils {
|
||||
Color? getDelayColor(int? delay) {
|
||||
if (delay == null) return null;
|
||||
if (delay < 0) return Colors.red;
|
||||
@@ -233,6 +233,63 @@ class Other {
|
||||
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() {
|
||||
return "${appName}_backup_${DateTime.now().show}.zip";
|
||||
}
|
||||
@@ -241,11 +298,6 @@ class Other {
|
||||
return "${appName}_${DateTime.now().show}.log";
|
||||
}
|
||||
|
||||
Size getScreenSize() {
|
||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
return view.physicalSize / view.devicePixelRatio;
|
||||
}
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
@@ -273,4 +325,4 @@ class Other {
|
||||
}
|
||||
}
|
||||
|
||||
final other = Other();
|
||||
final utils = Utils();
|
||||
@@ -21,12 +21,12 @@ class Window {
|
||||
await windowManager.ensureInitialized();
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(props.width, props.height),
|
||||
minimumSize: const Size(380, 500),
|
||||
minimumSize: const Size(380, 400),
|
||||
);
|
||||
if (!Platform.isMacOS || version > 10) {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
}
|
||||
if(!Platform.isMacOS){
|
||||
if (!Platform.isMacOS) {
|
||||
final left = props.left ?? 0;
|
||||
final top = props.top ?? 0;
|
||||
final right = left + props.width;
|
||||
@@ -36,7 +36,7 @@ class Window {
|
||||
} else {
|
||||
final displays = await screenRetriever.getAllDisplays();
|
||||
final isPositionValid = displays.any(
|
||||
(display) {
|
||||
(display) {
|
||||
final displayBounds = Rect.fromLTWH(
|
||||
display.visiblePosition!.dx,
|
||||
display.visiblePosition!.dy,
|
||||
@@ -69,8 +69,10 @@ class Window {
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
}
|
||||
|
||||
Future<bool> isVisible() async {
|
||||
return await windowManager.isVisible();
|
||||
Future<bool> get isVisible async {
|
||||
final value = await windowManager.isVisible();
|
||||
commonPrint.log("window visible check: $value");
|
||||
return value;
|
||||
}
|
||||
|
||||
close() async {
|
||||
|
||||
@@ -10,12 +10,14 @@ import 'package:fl_clash/common/archive.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'common/common.dart';
|
||||
import 'fragments/profiles/override_profile.dart';
|
||||
import 'models/models.dart';
|
||||
|
||||
class AppController {
|
||||
@@ -28,8 +30,9 @@ class AppController {
|
||||
AppController(this.context, WidgetRef ref) : _ref = ref;
|
||||
|
||||
updateClashConfigDebounce() {
|
||||
debouncer.call(DebounceTag.updateClashConfig, () {
|
||||
updateClashConfig(true);
|
||||
debouncer.call(DebounceTag.updateClashConfig, () async {
|
||||
final isPatch = globalState.appState.needApply ? false : true;
|
||||
await updateClashConfig(isPatch);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,7 +71,7 @@ class AppController {
|
||||
|
||||
restartCore() async {
|
||||
await clashService?.reStart();
|
||||
await initCore();
|
||||
await _initCore();
|
||||
|
||||
if (_ref.read(runTimeProvider.notifier).isStart) {
|
||||
await globalState.handleStart();
|
||||
@@ -98,7 +101,6 @@ class AppController {
|
||||
_ref.read(trafficsProvider.notifier).clear();
|
||||
_ref.read(totalTrafficProvider.notifier).value = Traffic();
|
||||
_ref.read(runTimeProvider.notifier).value = null;
|
||||
// tray.updateTrayTitle(null);
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
}
|
||||
@@ -151,7 +153,7 @@ class AppController {
|
||||
updateLocalIp() async {
|
||||
_ref.read(localIpProvider.notifier).value = null;
|
||||
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 {
|
||||
@@ -242,6 +244,7 @@ class AppController {
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig([bool? isPatch]) async {
|
||||
commonPrint.log("update clash patch: ${isPatch ?? false}");
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
@@ -280,6 +283,9 @@ class AppController {
|
||||
final res = await clashCore.updateConfig(
|
||||
globalState.getUpdateConfigParams(isPatch),
|
||||
);
|
||||
if (isPatch == false) {
|
||||
_ref.read(needApplyProvider.notifier).value = false;
|
||||
}
|
||||
if (res.isNotEmpty) throw res;
|
||||
lastTunEnable = enableTun;
|
||||
lastProfileModified = await profile?.profileLastModified;
|
||||
@@ -414,10 +420,13 @@ class AppController {
|
||||
Map<String, dynamic>? data,
|
||||
bool handleError = false,
|
||||
}) async {
|
||||
if (globalState.isPre) {
|
||||
return;
|
||||
}
|
||||
if (data != null) {
|
||||
final tagName = data['tag_name'];
|
||||
final body = data['body'];
|
||||
final submits = other.parseReleaseBody(body);
|
||||
final submits = utils.parseReleaseBody(body);
|
||||
final textTheme = context.textTheme;
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.discoverNewVersion,
|
||||
@@ -472,7 +481,7 @@ class AppController {
|
||||
await handleExit();
|
||||
}
|
||||
|
||||
Future<void> initCore() async {
|
||||
Future<void> _initCore() async {
|
||||
final isInit = await clashCore.isInit;
|
||||
if (!isInit) {
|
||||
await clashCore.setState(
|
||||
@@ -486,7 +495,7 @@ class AppController {
|
||||
init() async {
|
||||
await _handlePreference();
|
||||
await _handlerDisclaimer();
|
||||
await initCore();
|
||||
await _initCore();
|
||||
await _initStatus();
|
||||
updateTray(true);
|
||||
autoLaunch?.updateStatus(
|
||||
@@ -520,37 +529,12 @@ class AppController {
|
||||
_ref.read(delayDataSourceProvider.notifier).setDelay(delay);
|
||||
}
|
||||
|
||||
toPage(
|
||||
int index, {
|
||||
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);
|
||||
}
|
||||
toPage(PageLabel pageLabel) {
|
||||
_ref.read(currentPageLabelProvider.notifier).value = pageLabel;
|
||||
}
|
||||
|
||||
toProfiles() {
|
||||
final index = _ref.read(currentNavigationsStateProvider).value.indexWhere(
|
||||
(element) => element.label == PageLabel.profiles,
|
||||
);
|
||||
if (index != -1) {
|
||||
toPage(index);
|
||||
}
|
||||
toPage(PageLabel.profiles);
|
||||
}
|
||||
|
||||
initLink() {
|
||||
@@ -587,17 +571,8 @@ class AppController {
|
||||
Future<bool> showDisclaimer() async {
|
||||
return await globalState.showCommonDialog<bool>(
|
||||
dismissible: false,
|
||||
child: AlertDialog(
|
||||
title: Text(appLocalizations.disclaimer),
|
||||
content: Container(
|
||||
width: dialogCommonWidth,
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableText(
|
||||
appLocalizations.disclaimerDesc,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CommonDialog(
|
||||
title: appLocalizations.disclaimer,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -615,6 +590,9 @@ class AppController {
|
||||
child: Text(appLocalizations.agree),
|
||||
)
|
||||
],
|
||||
child: SelectableText(
|
||||
appLocalizations.disclaimerDesc,
|
||||
),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
@@ -680,9 +658,9 @@ class AppController {
|
||||
addProfileFormURL(url);
|
||||
}
|
||||
|
||||
updateViewWidth(double width) {
|
||||
updateViewSize(Size size) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ref.read(viewWidthProvider.notifier).value = width;
|
||||
_ref.read(viewSizeProvider.notifier).value = size;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -693,9 +671,9 @@ class AppController {
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) => other.sortByChar(
|
||||
other.getPinyin(a.name),
|
||||
other.getPinyin(b.name),
|
||||
(a, b) => utils.sortByChar(
|
||||
utils.getPinyin(a.name),
|
||||
utils.getPinyin(b.name),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -741,10 +719,18 @@ class AppController {
|
||||
final providersPath = await appPath.getProvidersPath(profileId);
|
||||
return await Isolate.run(() async {
|
||||
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) {
|
||||
await File(providersPath).delete(recursive: true);
|
||||
final providersFileDir = File(providersPath);
|
||||
final isExists = await providersFileDir.exists();
|
||||
if (isExists) {
|
||||
providersFileDir.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -798,10 +784,10 @@ class AppController {
|
||||
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(mode: mode),
|
||||
);
|
||||
// if (mode == Mode.global) {
|
||||
// updateCurrentGroupName(GroupName.GLOBAL.name);
|
||||
// }
|
||||
// addCheckIpNumDebounce();
|
||||
if (mode == Mode.global) {
|
||||
updateCurrentGroupName(GroupName.GLOBAL.name);
|
||||
}
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
updateAutoLaunch() {
|
||||
@@ -813,7 +799,7 @@ class AppController {
|
||||
}
|
||||
|
||||
updateVisible() async {
|
||||
final visible = await window?.isVisible();
|
||||
final visible = await window?.isVisible;
|
||||
if (visible != null && !visible) {
|
||||
window?.show();
|
||||
} else {
|
||||
@@ -836,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 {
|
||||
final logsRaw = _ref.read(logsProvider).list.map(
|
||||
(item) => item.toString(),
|
||||
@@ -845,7 +863,7 @@ class AppController {
|
||||
return utf8.encode(logsRawString);
|
||||
});
|
||||
return await picker.saveFile(
|
||||
other.logFile,
|
||||
utils.logFile,
|
||||
Uint8List.fromList(data),
|
||||
) !=
|
||||
null;
|
||||
|
||||
@@ -220,11 +220,12 @@ enum ProxiesIconStyle {
|
||||
|
||||
enum FontFamily {
|
||||
twEmoji("Twemoji"),
|
||||
jetBrainsMono("JetBrainsMono"),
|
||||
icon("Icons");
|
||||
|
||||
final String? value;
|
||||
final String value;
|
||||
|
||||
const FontFamily([this.value]);
|
||||
const FontFamily(this.value);
|
||||
}
|
||||
|
||||
enum RouteMode {
|
||||
@@ -384,3 +385,65 @@ enum PageLabel {
|
||||
resources,
|
||||
connections,
|
||||
}
|
||||
|
||||
enum RuleAction {
|
||||
DOMAIN("DOMAIN"),
|
||||
DOMAIN_SUFFIX("DOMAIN-SUFFIX"),
|
||||
DOMAIN_KEYWORD("DOMAIN-KEYWORD"),
|
||||
DOMAIN_REGEX("DOMAIN-REGEX"),
|
||||
GEOSITE("GEOSITE"),
|
||||
IP_CIDR("IP-CIDR"),
|
||||
IP_CIDR6("IP-CIDR6"),
|
||||
IP_SUFFIX("IP-SUFFIX"),
|
||||
IP_ASN("IP-ASN"),
|
||||
GEOIP("GEOIP"),
|
||||
SRC_GEOIP("SRC-GEOIP"),
|
||||
SRC_IP_ASN("SRC-IP-ASN"),
|
||||
SRC_IP_CIDR("SRC-IP-CIDR"),
|
||||
SRC_IP_SUFFIX("SRC-IP-SUFFIX"),
|
||||
DST_PORT("DST-PORT"),
|
||||
SRC_PORT("SRC-PORT"),
|
||||
IN_PORT("IN-PORT"),
|
||||
IN_TYPE("IN-TYPE"),
|
||||
IN_USER("IN-USER"),
|
||||
IN_NAME("IN-NAME"),
|
||||
PROCESS_PATH("PROCESS-PATH"),
|
||||
PROCESS_PATH_REGEX("PROCESS-PATH-REGEX"),
|
||||
PROCESS_NAME("PROCESS-NAME"),
|
||||
PROCESS_NAME_REGEX("PROCESS-NAME-REGEX"),
|
||||
UID("UID"),
|
||||
NETWORK("NETWORK"),
|
||||
DSCP("DSCP"),
|
||||
RULE_SET("RULE-SET"),
|
||||
AND("AND"),
|
||||
OR("OR"),
|
||||
NOT("NOT"),
|
||||
SUB_RULE("SUB-RULE"),
|
||||
MATCH("MATCH");
|
||||
|
||||
final String value;
|
||||
|
||||
const RuleAction(this.value);
|
||||
}
|
||||
|
||||
extension RuleActionExt on RuleAction {
|
||||
bool get hasParams => [
|
||||
RuleAction.GEOIP,
|
||||
RuleAction.IP_ASN,
|
||||
RuleAction.SRC_IP_ASN,
|
||||
RuleAction.IP_CIDR,
|
||||
RuleAction.IP_CIDR6,
|
||||
RuleAction.IP_SUFFIX,
|
||||
RuleAction.RULE_SET,
|
||||
].contains(this);
|
||||
}
|
||||
|
||||
enum OverrideRuleType {
|
||||
override,
|
||||
added,
|
||||
}
|
||||
|
||||
enum RuleTarget {
|
||||
DIRECT,
|
||||
REJECT,
|
||||
}
|
||||
|
||||
@@ -150,9 +150,17 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
final res = await showSheet<int>(
|
||||
title: appLocalizations.proxiesSetting,
|
||||
context: context,
|
||||
body: AccessControlPanel(),
|
||||
props: SheetProps(
|
||||
isScrollControlled: true,
|
||||
),
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: AccessControlPanel(),
|
||||
title: appLocalizations.proxiesSetting,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (res == 1) {
|
||||
_intelligentSelected();
|
||||
@@ -763,17 +771,19 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._buildModeSetting(),
|
||||
..._buildSortSetting(),
|
||||
..._buildSourceSetting(),
|
||||
..._buildActionSetting(),
|
||||
],
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._buildModeSetting(),
|
||||
..._buildSortSetting(),
|
||||
..._buildSourceSetting(),
|
||||
..._buildActionSetting(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -276,8 +276,8 @@ class ApplicationSettingFragment extends StatelessWidget {
|
||||
AutoRunItem(),
|
||||
if (Platform.isAndroid) ...[
|
||||
HiddenItem(),
|
||||
AnimateTabItem(),
|
||||
],
|
||||
AnimateTabItem(),
|
||||
OpenLogsItem(),
|
||||
CloseConnectionsItem(),
|
||||
UsageItem(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/dialog.dart';
|
||||
import 'package:fl_clash/widgets/fade_box.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
import 'package:fl_clash/widgets/text.dart';
|
||||
@@ -74,7 +75,7 @@ class BackupAndRecovery extends ConsumerWidget {
|
||||
() async {
|
||||
final backupData = await globalState.appController.backupData();
|
||||
final value = await picker.saveFile(
|
||||
other.getBackupFileName(),
|
||||
utils.getBackupFileName(),
|
||||
Uint8List.fromList(backupData),
|
||||
);
|
||||
if (value == null) return false;
|
||||
@@ -174,7 +175,7 @@ class BackupAndRecovery extends ConsumerWidget {
|
||||
future: client!.pingCompleter.future,
|
||||
builder: (_, snapshot) {
|
||||
return Center(
|
||||
child: FadeBox(
|
||||
child: FadeThroughBox(
|
||||
child: snapshot.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const SizedBox(
|
||||
@@ -275,30 +276,27 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(appLocalizations.recovery),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
return CommonDialog(
|
||||
title: appLocalizations.recovery,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 16,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleOnTab(RecoveryOption.onlyProfiles);
|
||||
},
|
||||
title: Text(appLocalizations.recoveryProfiles),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleOnTab(RecoveryOption.all);
|
||||
},
|
||||
title: Text(appLocalizations.recoveryAll),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleOnTab(RecoveryOption.onlyProfiles);
|
||||
},
|
||||
title: Text(appLocalizations.recoveryProfiles),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleOnTab(RecoveryOption.all);
|
||||
},
|
||||
title: Text(appLocalizations.recoveryAll),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -351,78 +349,8 @@ class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(appLocalizations.webDAVConfiguration),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: SizedBox(
|
||||
width: dialogCommonWidth,
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: uriController,
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.link),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: appLocalizations.address,
|
||||
helperText: appLocalizations.addressHelp,
|
||||
),
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty || !value.isUrl) {
|
||||
return appLocalizations.addressTip;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: userController,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.account_circle),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: appLocalizations.account,
|
||||
),
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return appLocalizations.accountTip;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _obscureController,
|
||||
builder: (_, obscure, __) {
|
||||
return TextFormField(
|
||||
controller: passwordController,
|
||||
obscureText: obscure,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.password),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
_obscureController.value = !obscure;
|
||||
},
|
||||
),
|
||||
labelText: appLocalizations.password,
|
||||
),
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return appLocalizations.passwordTip;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
return CommonDialog(
|
||||
title: appLocalizations.webDAVConfiguration,
|
||||
actions: [
|
||||
if (widget.dav != null)
|
||||
TextButton(
|
||||
@@ -434,6 +362,73 @@ class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
|
||||
child: Text(appLocalizations.save),
|
||||
)
|
||||
],
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: uriController,
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.link),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: appLocalizations.address,
|
||||
helperText: appLocalizations.addressHelp,
|
||||
),
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty || !value.isUrl) {
|
||||
return appLocalizations.addressTip;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: userController,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.account_circle),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: appLocalizations.account,
|
||||
),
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return appLocalizations.accountTip;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _obscureController,
|
||||
builder: (_, obscure, __) {
|
||||
return TextFormField(
|
||||
controller: passwordController,
|
||||
obscureText: obscure,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.password),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
_obscureController.value = !obscure;
|
||||
},
|
||||
),
|
||||
labelText: appLocalizations.password,
|
||||
),
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return appLocalizations.passwordTip;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/general.dart';
|
||||
import 'package:fl_clash/fragments/config/network.dart';
|
||||
import 'package:fl_clash/models/clash_config.dart';
|
||||
import 'package:fl_clash/providers/config.dart' show patchClashConfigProvider;
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../state.dart';
|
||||
|
||||
class ConfigFragment extends StatefulWidget {
|
||||
const ConfigFragment({super.key});
|
||||
@@ -16,18 +21,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> items = [
|
||||
ListItem.open(
|
||||
title: Text(appLocalizations.network),
|
||||
subtitle: Text(appLocalizations.networkDesc),
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.network,
|
||||
isScaffold: true,
|
||||
isBlur: false,
|
||||
extendPageWidth: 360,
|
||||
widget: const NetworkListView(),
|
||||
),
|
||||
),
|
||||
ListItem.open(
|
||||
title: Text(appLocalizations.general),
|
||||
subtitle: Text(appLocalizations.generalDesc),
|
||||
@@ -37,20 +30,51 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
widget: generateListView(
|
||||
generalItems,
|
||||
),
|
||||
isBlur: false,
|
||||
extendPageWidth: 360,
|
||||
blur: false,
|
||||
),
|
||||
),
|
||||
ListItem.open(
|
||||
title: Text(appLocalizations.network),
|
||||
subtitle: Text(appLocalizations.networkDesc),
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.network,
|
||||
blur: false,
|
||||
widget: const NetworkListView(),
|
||||
),
|
||||
),
|
||||
ListItem.open(
|
||||
title: const Text("DNS"),
|
||||
subtitle: Text(appLocalizations.dnsDesc),
|
||||
leading: const Icon(Icons.dns),
|
||||
delegate: const OpenDelegate(
|
||||
delegate: OpenDelegate(
|
||||
title: "DNS",
|
||||
widget: DnsListView(),
|
||||
isScaffold: true,
|
||||
isBlur: false,
|
||||
extendPageWidth: 360,
|
||||
action: Consumer(builder: (_, ref, __) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
dns: defaultDns,
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
Icons.replay,
|
||||
),
|
||||
);
|
||||
}),
|
||||
widget: const DnsListView(),
|
||||
blur: false,
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -205,7 +203,7 @@ class FakeIpFilterItem extends StatelessWidget {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.fakeipFilter),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.fakeipFilter,
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
@@ -213,7 +211,7 @@ class FakeIpFilterItem extends StatelessWidget {
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fakeIpFilter),
|
||||
);
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: appLocalizations.fakeipFilter,
|
||||
items: fakeIpFilter,
|
||||
titleBuilder: (item) => Text(item),
|
||||
@@ -227,7 +225,6 @@ class FakeIpFilterItem extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -242,14 +239,14 @@ class DefaultNameserverItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.defaultNameserver),
|
||||
subtitle: Text(appLocalizations.defaultNameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.defaultNameserver,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final defaultNameserver = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.defaultNameserver),
|
||||
);
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
@@ -262,7 +259,6 @@ class DefaultNameserverItem extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -278,13 +274,13 @@ class NameserverItem extends StatelessWidget {
|
||||
subtitle: Text(appLocalizations.nameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.nameserver,
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final nameserver = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.nameserver),
|
||||
);
|
||||
return ListPage(
|
||||
title: "域名服务器",
|
||||
return ListInputPage(
|
||||
title: appLocalizations.nameserver,
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
@@ -296,7 +292,6 @@ class NameserverItem extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -357,28 +352,27 @@ class NameserverPolicyItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.nameserverPolicy),
|
||||
subtitle: Text(appLocalizations.nameserverPolicyDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final nameserverPolicy = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.nameserverPolicy),
|
||||
);
|
||||
return ListPage(
|
||||
return MapInputPage(
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
items: nameserverPolicy.entries,
|
||||
map: nameserverPolicy,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items) {
|
||||
onChange: (value) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns(
|
||||
nameserverPolicy: Map.fromEntries(items),
|
||||
nameserverPolicy: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -393,7 +387,7 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.proxyNameserver),
|
||||
subtitle: Text(appLocalizations.proxyNameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.proxyNameserver,
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
@@ -401,7 +395,7 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.proxyServerNameserver),
|
||||
);
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: appLocalizations.proxyNameserver,
|
||||
items: proxyServerNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
@@ -415,7 +409,6 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -430,13 +423,13 @@ class FallbackItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.fallback),
|
||||
subtitle: Text(appLocalizations.fallbackDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.fallback,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final fallback = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.fallback),
|
||||
);
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
@@ -449,7 +442,6 @@ class FallbackItem extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -518,14 +510,14 @@ class GeositeItem extends StatelessWidget {
|
||||
return ListItem.open(
|
||||
title: const Text("Geosite"),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: "Geosite",
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final geosite = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.geosite),
|
||||
);
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
@@ -538,7 +530,6 @@ class GeositeItem extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -552,14 +543,14 @@ class IpcidrItem extends StatelessWidget {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.ipcidr),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.ipcidr,
|
||||
widget: Consumer(builder: (_, ref, ___) {
|
||||
final ipcidr = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.ipcidr),
|
||||
);
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
@@ -572,7 +563,6 @@ class IpcidrItem extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -586,14 +576,14 @@ class DomainItem extends StatelessWidget {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.domain),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.domain,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final domain = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.domain),
|
||||
);
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
@@ -606,7 +596,6 @@ class DomainItem extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -671,39 +660,8 @@ const dnsItems = <Widget>[
|
||||
class DnsListView extends ConsumerWidget {
|
||||
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
|
||||
Widget build(BuildContext context, ref) {
|
||||
_initActions(context, ref);
|
||||
return generateListView(
|
||||
dnsItems,
|
||||
);
|
||||
|
||||
@@ -199,28 +199,27 @@ class HostsItem extends StatelessWidget {
|
||||
title: const Text("Hosts"),
|
||||
subtitle: Text(appLocalizations.hostsDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: "Hosts",
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final hosts = ref
|
||||
.watch(patchClashConfigProvider.select((state) => state.hosts));
|
||||
return ListPage(
|
||||
return MapInputPage(
|
||||
title: "Hosts",
|
||||
items: hosts.entries,
|
||||
map: hosts,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items) {
|
||||
onChange: (value) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
hosts: Map.fromEntries(items),
|
||||
hosts: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,15 +224,14 @@ class BypassDomainItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.bypassDomain),
|
||||
subtitle: Text(appLocalizations.bypassDomainDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
blur: false,
|
||||
title: appLocalizations.bypassDomain,
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
_initActions(context, ref);
|
||||
final bypassDomain = ref.watch(
|
||||
networkSettingProvider.select((state) => state.bypassDomain));
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: appLocalizations.bypassDomain,
|
||||
items: bypassDomain,
|
||||
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),
|
||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
blur: false,
|
||||
maxWidth: 360,
|
||||
title: appLocalizations.routeAddress,
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final routeAddress = ref.watch(patchClashConfigProvider
|
||||
.select((state) => state.tun.routeAddress));
|
||||
return ListPage(
|
||||
return ListInputPage(
|
||||
title: appLocalizations.routeAddress,
|
||||
items: routeAddress,
|
||||
titleBuilder: (item) => Text(item),
|
||||
@@ -319,7 +317,6 @@ class RouteAddressItem extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
|
||||
return ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
onClickKeyword: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
trailing: IconButton(
|
||||
|
||||
@@ -9,40 +9,15 @@ import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class FindProcessBuilder extends StatelessWidget {
|
||||
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 {
|
||||
class ConnectionItem extends ConsumerWidget {
|
||||
final Connection connection;
|
||||
final Function(String)? onClick;
|
||||
final Function(String)? onClickKeyword;
|
||||
final Widget? trailing;
|
||||
|
||||
const ConnectionItem({
|
||||
super.key,
|
||||
required this.connection,
|
||||
this.onClick,
|
||||
this.onClickKeyword,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
@@ -59,7 +34,14 @@ class ConnectionItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
@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(
|
||||
connection.desc,
|
||||
style: context.textTheme.bodyLarge,
|
||||
@@ -86,70 +68,143 @@ class ConnectionItem extends StatelessWidget {
|
||||
CommonChip(
|
||||
label: chain,
|
||||
onPressed: () {
|
||||
if (onClick == null) return;
|
||||
onClick!(chain);
|
||||
if (onClickKeyword == null) return;
|
||||
onClickKeyword!(chain);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!Platform.isAndroid) {
|
||||
return ListItem(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||
title: title,
|
||||
subtitle: subTitle,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
return FindProcessBuilder(
|
||||
builder: (bool value) {
|
||||
final leading = value
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
if (onClick == null) return;
|
||||
final process = connection.metadata.process;
|
||||
if (process.isEmpty) return;
|
||||
onClick!(process);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: FutureBuilder<ImageProvider?>(
|
||||
future: _getPackageIcon(connection),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Image(
|
||||
image: snapshot.data!,
|
||||
gaplessPlayback: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
return CommonPopupBox(
|
||||
targetBuilder: (open) {
|
||||
return ListItem(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
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,
|
||||
subtitle: subTitle,
|
||||
trailing: trailing,
|
||||
);
|
||||
// return InkWell(
|
||||
// child: GestureDetector(
|
||||
// onLongPressStart: (details) {
|
||||
// if (!system.isDesktop) {
|
||||
// return;
|
||||
// }
|
||||
// open(
|
||||
// offset: details.localPosition.translate(
|
||||
// 0,
|
||||
// -12,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// onSecondaryTapDown: (details) {
|
||||
// if (!system.isDesktop) {
|
||||
// return;
|
||||
// }
|
||||
// open(
|
||||
// offset: details.localPosition.translate(
|
||||
// 0,
|
||||
// -12,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// child: ListItem(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 16,
|
||||
// vertical: 4,
|
||||
// ),
|
||||
// tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||
// leading: value
|
||||
// ? GestureDetector(
|
||||
// onTap: () {
|
||||
// if (onClickKeyword == null) return;
|
||||
// final process = connection.metadata.process;
|
||||
// if (process.isEmpty) return;
|
||||
// onClickKeyword!(process);
|
||||
// },
|
||||
// child: Container(
|
||||
// margin: const EdgeInsets.only(top: 4),
|
||||
// width: 48,
|
||||
// height: 48,
|
||||
// child: FutureBuilder<ImageProvider?>(
|
||||
// future: _getPackageIcon(connection),
|
||||
// builder: (_, snapshot) {
|
||||
// if (!snapshot.hasData && snapshot.data == null) {
|
||||
// return Container();
|
||||
// } else {
|
||||
// return Image(
|
||||
// image: snapshot.data!,
|
||||
// gaplessPlayback: true,
|
||||
// width: 48,
|
||||
// height: 48,
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// : null,
|
||||
// title: title,
|
||||
// subtitle: subTitle,
|
||||
// trailing: trailing,
|
||||
// ),
|
||||
// ),
|
||||
// onTap: () {},
|
||||
// );
|
||||
},
|
||||
popup: CommonPopupMenu(
|
||||
minWidth: 160,
|
||||
items: [
|
||||
PopupMenuItemData(
|
||||
label: "编辑规则",
|
||||
onPressed: () {
|
||||
// _handleShowEditExtendPage(context);
|
||||
},
|
||||
),
|
||||
PopupMenuItemData(
|
||||
label: "设置直连",
|
||||
onPressed: () {},
|
||||
),
|
||||
PopupMenuItemData(
|
||||
label: "一键屏蔽",
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
@@ -9,8 +11,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'item.dart';
|
||||
|
||||
double _preOffset = 0;
|
||||
|
||||
class RequestsFragment extends ConsumerStatefulWidget {
|
||||
const RequestsFragment({super.key});
|
||||
|
||||
@@ -20,15 +20,12 @@ class RequestsFragment extends ConsumerStatefulWidget {
|
||||
|
||||
class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
with PageMixin {
|
||||
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
|
||||
final _requestsStateNotifier =
|
||||
ValueNotifier<ConnectionsState>(const ConnectionsState());
|
||||
List<Connection> _requests = [];
|
||||
|
||||
final ScrollController _scrollController = ScrollController(
|
||||
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||
);
|
||||
|
||||
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||
final _cacheKey = ValueKey("requests_list");
|
||||
late ScrollController _scrollController;
|
||||
|
||||
double _currentMaxWidth = 0;
|
||||
|
||||
@@ -48,10 +45,13 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1;
|
||||
_scrollController = ScrollController(
|
||||
initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite,
|
||||
);
|
||||
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||
connections: globalState.appState.requests.list,
|
||||
);
|
||||
|
||||
ref.listenManual(
|
||||
isCurrentPageProvider(
|
||||
PageLabel.requests,
|
||||
@@ -78,10 +78,6 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
}
|
||||
|
||||
double _calcCacheHeight(Connection item) {
|
||||
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
|
||||
if (cacheHeight != null) {
|
||||
return cacheHeight;
|
||||
}
|
||||
final size = globalState.measure.computeTextSize(
|
||||
Text(
|
||||
item.desc,
|
||||
@@ -102,14 +98,13 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
final lines = (chainSize.height / baseHeight).round();
|
||||
final computerHeight =
|
||||
size.height + chainSize.height + 24 + 24 * (lines - 1);
|
||||
_cacheDynamicHeightMap.put(item.id, computerHeight);
|
||||
return computerHeight;
|
||||
}
|
||||
|
||||
_handleTryClearCache(double maxWidth) {
|
||||
if (_currentMaxWidth != maxWidth) {
|
||||
_currentMaxWidth = maxWidth;
|
||||
_cacheDynamicHeightMap.clear();
|
||||
_key.currentState?.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +113,6 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
_requestsStateNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
_currentMaxWidth = 0;
|
||||
_cacheDynamicHeightMap.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -143,9 +137,19 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
return FindProcessBuilder(builder: (value) {
|
||||
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
|
||||
return ValueListenableBuilder<ConnectionsState>(
|
||||
return Consumer(
|
||||
builder: (_, ref, child) {
|
||||
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,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
@@ -159,7 +163,7 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
(connection) => ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
onClickKeyword: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
),
|
||||
@@ -172,19 +176,19 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
.toList();
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
},
|
||||
child: ScrollToEndBox(
|
||||
controller: _scrollController,
|
||||
cacheKey: _cacheKey,
|
||||
dataSource: connections,
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
child: CacheItemExtentListView(
|
||||
key: _key,
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemExtentBuilder: (index, __) {
|
||||
itemExtentBuilder: (index) {
|
||||
final widget = items[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return 0;
|
||||
@@ -199,13 +203,21 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||
return items[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
keyBuilder: (int index) {
|
||||
final widget = items[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return "divider";
|
||||
}
|
||||
final connection = connections[(index / 2).floor()];
|
||||
return connection.id;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class IntranetIP extends StatelessWidget {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final localIp = ref.watch(localIpProvider);
|
||||
return FadeBox(
|
||||
return FadeThroughBox(
|
||||
child: localIp != null
|
||||
? TooltipText(
|
||||
text: Text(
|
||||
|
||||
@@ -39,7 +39,7 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
_memoryInfoStateNotifier.value = TrafficValue(
|
||||
value: clashLib != null ? rss : await clashCore.getMemory() + rss,
|
||||
);
|
||||
timer = Timer(Duration(seconds: 5), () async {
|
||||
timer = Timer(Duration(seconds: 2), () async {
|
||||
_updateMemory();
|
||||
});
|
||||
});
|
||||
@@ -47,13 +47,8 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
|
||||
@override
|
||||
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(
|
||||
height: getWidgetHeight(2),
|
||||
height: getWidgetHeight(1),
|
||||
child: CommonCard(
|
||||
info: Info(
|
||||
iconData: Icons.memory,
|
||||
@@ -76,43 +71,94 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
children: [
|
||||
Text(
|
||||
trafficValue.showValue,
|
||||
style: context.textTheme.titleLarge?.toLight,
|
||||
style:
|
||||
context.textTheme.bodyMedium?.toLight.adjustSize(1),
|
||||
),
|
||||
SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(
|
||||
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,
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -12,7 +12,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||
const NetworkDetectionState(
|
||||
isTesting: true,
|
||||
isTesting: false,
|
||||
isLoading: true,
|
||||
ipInfo: null,
|
||||
),
|
||||
);
|
||||
@@ -28,7 +29,6 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
bool? _preIsStart;
|
||||
Timer? _setTimeoutTimer;
|
||||
CancelToken? cancelToken;
|
||||
Completer? checkedCompleter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -37,11 +37,14 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
_startCheck();
|
||||
}
|
||||
});
|
||||
if (!_networkDetectionState.value.isTesting &&
|
||||
_networkDetectionState.value.isLoading) {
|
||||
_startCheck();
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
_startCheck() async {
|
||||
await checkedCompleter?.future;
|
||||
if (cancelToken != null) {
|
||||
cancelToken!.cancel();
|
||||
cancelToken = null;
|
||||
@@ -64,7 +67,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
}
|
||||
_clearSetTimeoutTimer();
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
isLoading: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
_preIsStart = isStart;
|
||||
@@ -74,16 +77,16 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
}
|
||||
cancelToken = CancelToken();
|
||||
try {
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
);
|
||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
);
|
||||
if (ipInfo != null) {
|
||||
checkedCompleter = Completer();
|
||||
checkedCompleter?.complete(
|
||||
Future.delayed(
|
||||
Duration(milliseconds: 3000),
|
||||
),
|
||||
);
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
isLoading: false,
|
||||
ipInfo: ipInfo,
|
||||
);
|
||||
return;
|
||||
@@ -91,14 +94,14 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
_clearSetTimeoutTimer();
|
||||
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
isLoading: false,
|
||||
ipInfo: null,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.toString() == "cancelled") {
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
isLoading: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
}
|
||||
@@ -136,7 +139,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
valueListenable: _networkDetectionState,
|
||||
builder: (_, state, __) {
|
||||
final ipInfo = state.ipInfo;
|
||||
final isTesting = state.isTesting;
|
||||
final isLoading = state.isLoading;
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Column(
|
||||
@@ -218,7 +221,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
),
|
||||
child: SizedBox(
|
||||
height: globalState.measure.bodyMediumHeight + 2,
|
||||
child: FadeBox(
|
||||
child: FadeThroughBox(
|
||||
child: ipInfo != null
|
||||
? TooltipText(
|
||||
text: Text(
|
||||
@@ -229,8 +232,8 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: FadeBox(
|
||||
child: isTesting == false && ipInfo == null
|
||||
: FadeThroughBox(
|
||||
child: isLoading == false && ipInfo == null
|
||||
? Text(
|
||||
"timeout",
|
||||
style: context.textTheme.bodyMedium
|
||||
|
||||
@@ -41,7 +41,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = context.colorScheme.onSurfaceVariant.toLight;
|
||||
final color = context.colorScheme.onSurfaceVariant.opacity80;
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(2),
|
||||
child: CommonCard(
|
||||
|
||||
@@ -38,7 +38,7 @@ class OutboundMode extends StatelessWidget {
|
||||
for (final item in Mode.values)
|
||||
Flexible(
|
||||
child: ListItem.radio(
|
||||
prue: true,
|
||||
dense: true,
|
||||
horizontalTitleGap: 4,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
|
||||
@@ -16,13 +16,20 @@ class TUNButton extends StatelessWidget {
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
body: generateListView(generateSection(
|
||||
items: [
|
||||
if (system.isDesktop) const TUNItem(),
|
||||
const TunStackItem(),
|
||||
],
|
||||
)),
|
||||
title: appLocalizations.tun,
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: generateListView(
|
||||
generateSection(
|
||||
items: [
|
||||
if (system.isDesktop) const TUNItem(),
|
||||
const TunStackItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: appLocalizations.tun,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
@@ -89,15 +96,20 @@ class SystemProxyButton extends StatelessWidget {
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
body: generateListView(
|
||||
generateSection(
|
||||
items: [
|
||||
SystemProxyItem(),
|
||||
BypassDomainItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: appLocalizations.systemProxy,
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: generateListView(
|
||||
generateSection(
|
||||
items: [
|
||||
SystemProxyItem(),
|
||||
BypassDomainItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: appLocalizations.systemProxy,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
|
||||
@@ -71,10 +71,10 @@ class _StartButtonState extends State<StartButton>
|
||||
final textWidth = globalState.measure
|
||||
.computeTextSize(
|
||||
Text(
|
||||
other.getTimeDifference(
|
||||
utils.getTimeDifference(
|
||||
DateTime.now(),
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||
style: context.textTheme.titleMedium?.toSoftBold,
|
||||
),
|
||||
)
|
||||
.width +
|
||||
@@ -123,10 +123,12 @@ class _StartButtonState extends State<StartButton>
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final runTime = ref.watch(runTimeProvider);
|
||||
final text = other.getTimeText(runTime);
|
||||
final text = utils.getTimeText(runTime);
|
||||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold.copyWith(
|
||||
color: context.colorScheme.onPrimaryContainer
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -51,10 +51,8 @@ class TrafficUsage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor =
|
||||
context.colorScheme.surfaceContainer.blendDarken(context, factor: 0.2);
|
||||
final secondaryColor =
|
||||
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3);
|
||||
final primaryColor = globalState.theme.darken3PrimaryContainer;
|
||||
final secondaryColor = globalState.theme.darken2SecondaryContainer;
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(2),
|
||||
child: CommonCard(
|
||||
|
||||
@@ -4,6 +4,7 @@ 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/card.dart';
|
||||
import 'package:fl_clash/widgets/dialog.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -156,9 +157,28 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(IntlExt.actionMessage((widget.hotKeyAction.action.name))),
|
||||
content: ValueListenableBuilder(
|
||||
return CommonDialog(
|
||||
title: IntlExt.actionMessage(widget.hotKeyAction.action.name),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_handleRemove();
|
||||
},
|
||||
child: Text(appLocalizations.remove),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_handleConfirm();
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.confirm,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: hotKeyActionNotifier,
|
||||
builder: (_, hotKeyAction, ___) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/models.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
double _preOffset = 0;
|
||||
|
||||
class LogsFragment extends ConsumerStatefulWidget {
|
||||
const LogsFragment({super.key});
|
||||
|
||||
@@ -19,17 +17,20 @@ class LogsFragment extends ConsumerStatefulWidget {
|
||||
|
||||
class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
|
||||
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
|
||||
final _scrollController = ScrollController(
|
||||
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||
);
|
||||
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||
final _cacheKey = ValueKey("logs_list");
|
||||
late ScrollController _scrollController;
|
||||
double _currentMaxWidth = 0;
|
||||
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
|
||||
|
||||
List<Log> _logs = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1;
|
||||
_scrollController = ScrollController(
|
||||
initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite,
|
||||
);
|
||||
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||
logs: globalState.appState.logs.list,
|
||||
);
|
||||
@@ -90,14 +91,13 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
|
||||
void dispose() {
|
||||
_logsStateNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
_cacheDynamicHeightMap.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_handleTryClearCache(double maxWidth) {
|
||||
if (_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) {
|
||||
final measure = globalState.measure;
|
||||
final bodySmallHeight = measure.bodySmallHeight;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -189,14 +181,14 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
},
|
||||
return ScrollToEndBox<Log>(
|
||||
controller: _scrollController,
|
||||
cacheKey: _cacheKey,
|
||||
dataSource: logs,
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
child: CacheItemExtentListView(
|
||||
key: _key,
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
@@ -204,7 +196,7 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemExtentBuilder: (index, __) {
|
||||
itemExtentBuilder: (index) {
|
||||
final item = items[index];
|
||||
if (item.runtimeType == Divider) {
|
||||
return 0;
|
||||
@@ -213,6 +205,14 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
|
||||
return _getItemHeight(log);
|
||||
},
|
||||
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; // 禁用过度滚动效果
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,19 +50,19 @@ class AddProfile extends StatelessWidget {
|
||||
return ListView(
|
||||
children: [
|
||||
ListItem(
|
||||
leading: const Icon(Icons.qr_code),
|
||||
leading: const Icon(Icons.qr_code_sharp),
|
||||
title: Text(appLocalizations.qrcode),
|
||||
subtitle: Text(appLocalizations.qrcodeDesc),
|
||||
onTap: _toScan,
|
||||
),
|
||||
ListItem(
|
||||
leading: const Icon(Icons.upload_file),
|
||||
leading: const Icon(Icons.upload_file_sharp),
|
||||
title: Text(appLocalizations.file),
|
||||
subtitle: Text(appLocalizations.fileDesc),
|
||||
onTap: _handleAddProfileFormFile,
|
||||
),
|
||||
ListItem(
|
||||
leading: const Icon(Icons.cloud_download),
|
||||
leading: const Icon(Icons.cloud_download_sharp),
|
||||
title: Text(appLocalizations.url),
|
||||
subtitle: Text(appLocalizations.urlDesc),
|
||||
onTap: _toAdd,
|
||||
@@ -90,9 +90,15 @@ class _URLFormDialogState extends State<URLFormDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(appLocalizations.importFromURL),
|
||||
content: SizedBox(
|
||||
return CommonDialog(
|
||||
title: appLocalizations.importFromURL,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _handleAddProfileFormURL,
|
||||
child: Text(appLocalizations.submit),
|
||||
)
|
||||
],
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
@@ -109,12 +115,6 @@ class _URLFormDialogState extends State<URLFormDialog> {
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _handleAddProfileFormURL,
|
||||
child: Text(appLocalizations.submit),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ class _EditProfileState extends State<EditProfile> {
|
||||
ValueListenableBuilder<FileInfo?>(
|
||||
valueListenable: fileInfoNotifier,
|
||||
builder: (_, fileInfo, __) {
|
||||
return FadeBox(
|
||||
return FadeThroughBox(
|
||||
child: fileInfo == null
|
||||
? Container()
|
||||
: ListItem(
|
||||
@@ -324,15 +324,13 @@ class _EditProfileState extends State<EditProfile> {
|
||||
},
|
||||
),
|
||||
];
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, __) {
|
||||
if (didPop) return;
|
||||
return CommonPopScope(
|
||||
onPop: () {
|
||||
if (fileData == null) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
_handleBack();
|
||||
return false;
|
||||
},
|
||||
child: FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import 'package:fl_clash/clash/core.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/card.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GenProfile extends StatefulWidget {
|
||||
final String profileId;
|
||||
|
||||
const GenProfile({
|
||||
super.key,
|
||||
required this.profileId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GenProfile> createState() => _GenProfileState();
|
||||
}
|
||||
|
||||
class _GenProfileState extends State<GenProfile> {
|
||||
final _currentClashConfigNotifier = ValueNotifier<ClashConfigSnippet?>(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 Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 100,
|
||||
mainAxisExtent: 50,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: clashConfig.proxyGroups.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
clashConfig.proxyGroups[index].name,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SliverList.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final rule = clashConfig.rule[index];
|
||||
return Text(
|
||||
rule,
|
||||
);
|
||||
},
|
||||
itemCount: clashConfig.rule.length,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: "自定义",
|
||||
);
|
||||
}
|
||||
}
|
||||
958
lib/fragments/profiles/override_profile.dart
Normal file
958
lib/fragments/profiles/override_profile.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:ui';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.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/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -11,7 +12,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'add_profile.dart';
|
||||
import 'gen_profile.dart';
|
||||
|
||||
class ProfilesFragment extends StatefulWidget {
|
||||
const ProfilesFragment({super.key});
|
||||
@@ -24,12 +24,17 @@ class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
|
||||
Function? applyConfigDebounce;
|
||||
|
||||
_handleShowAddExtendPage() {
|
||||
showExtendPage(
|
||||
showExtend(
|
||||
globalState.navigatorKey.currentState!.context,
|
||||
body: AddProfile(
|
||||
context: globalState.navigatorKey.currentState!.context,
|
||||
),
|
||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: AddProfile(
|
||||
context: globalState.navigatorKey.currentState!.context,
|
||||
),
|
||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,12 +86,13 @@ class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
|
||||
onPressed: () {
|
||||
final profiles = globalState.config.profiles;
|
||||
showSheet(
|
||||
title: appLocalizations.profilesSort,
|
||||
context: context,
|
||||
body: SizedBox(
|
||||
height: 400,
|
||||
child: ReorderableProfiles(profiles: profiles),
|
||||
),
|
||||
builder: (_, type) {
|
||||
return ReorderableProfilesSheet(
|
||||
type: type,
|
||||
profiles: profiles,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.sort),
|
||||
@@ -205,13 +211,18 @@ class ProfileItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
_handleShowEditExtendPage(BuildContext context) {
|
||||
showExtendPage(
|
||||
showExtend(
|
||||
context,
|
||||
body: EditProfile(
|
||||
profile: profile,
|
||||
context: context,
|
||||
),
|
||||
title: "${appLocalizations.edit}${appLocalizations.profile}",
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: EditProfile(
|
||||
profile: profile,
|
||||
context: context,
|
||||
),
|
||||
title: "${appLocalizations.edit}${appLocalizations.profile}",
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -277,7 +288,7 @@ class ProfileItem extends StatelessWidget {
|
||||
_handlePushGenProfilePage(BuildContext context, String id) {
|
||||
BaseNavigator.push(
|
||||
context,
|
||||
GenProfile(
|
||||
OverrideProfile(
|
||||
profileId: id,
|
||||
),
|
||||
);
|
||||
@@ -285,7 +296,6 @@ class ProfileItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final key = GlobalKey<CommonPopupBoxState>();
|
||||
return CommonCard(
|
||||
isSelected: profile.id == groupValue,
|
||||
onPressed: () {
|
||||
@@ -298,17 +308,16 @@ class ProfileItem extends StatelessWidget {
|
||||
trailing: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: FadeBox(
|
||||
child: FadeThroughBox(
|
||||
child: profile.isUpdating
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonPopupBox(
|
||||
key: key,
|
||||
popup: CommonPopupMenu(
|
||||
items: [
|
||||
ActionItemData(
|
||||
PopupMenuItemData(
|
||||
icon: Icons.edit_outlined,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
@@ -316,52 +325,47 @@ class ProfileItem extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
if (profile.type == ProfileType.url) ...[
|
||||
ActionItemData(
|
||||
PopupMenuItemData(
|
||||
icon: Icons.sync_alt_sharp,
|
||||
label: appLocalizations.sync,
|
||||
onPressed: () {
|
||||
updateProfile();
|
||||
},
|
||||
),
|
||||
// ActionItemData(
|
||||
// icon: Icons.copy,
|
||||
// label: appLocalizations.copyLink,
|
||||
// onPressed: () {
|
||||
// _handleCopyLink(context);
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
// ActionItemData(
|
||||
// icon: Icons.extension_outlined,
|
||||
// label: "自定义",
|
||||
// onPressed: () {
|
||||
// _handlePushGenProfilePage(context, profile.id);
|
||||
// },
|
||||
// ),
|
||||
ActionItemData(
|
||||
PopupMenuItemData(
|
||||
icon: Icons.extension_outlined,
|
||||
label: appLocalizations.override,
|
||||
onPressed: () {
|
||||
_handlePushGenProfilePage(context, profile.id);
|
||||
},
|
||||
),
|
||||
PopupMenuItemData(
|
||||
icon: Icons.file_copy_outlined,
|
||||
label: appLocalizations.exportFile,
|
||||
onPressed: () {
|
||||
_handleExportFile(context);
|
||||
},
|
||||
),
|
||||
ActionItemData(
|
||||
PopupMenuItemData(
|
||||
icon: Icons.delete_outlined,
|
||||
iconSize: 20,
|
||||
label: appLocalizations.delete,
|
||||
onPressed: () {
|
||||
_handleDeleteProfile(context);
|
||||
},
|
||||
type: ActionType.danger,
|
||||
type: PopupMenuItemType.danger,
|
||||
),
|
||||
],
|
||||
),
|
||||
target: IconButton(
|
||||
onPressed: () {
|
||||
key.currentState?.pop();
|
||||
},
|
||||
icon: Icon(Icons.more_vert),
|
||||
),
|
||||
targetBuilder: (open) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
open();
|
||||
},
|
||||
icon: Icon(Icons.more_vert),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -397,19 +401,22 @@ class ProfileItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ReorderableProfiles extends StatefulWidget {
|
||||
class ReorderableProfilesSheet extends StatefulWidget {
|
||||
final List<Profile> profiles;
|
||||
final SheetType type;
|
||||
|
||||
const ReorderableProfiles({
|
||||
const ReorderableProfilesSheet({
|
||||
super.key,
|
||||
required this.profiles,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReorderableProfiles> createState() => _ReorderableProfilesState();
|
||||
State<ReorderableProfilesSheet> createState() =>
|
||||
_ReorderableProfilesSheetState();
|
||||
}
|
||||
|
||||
class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
||||
class _ReorderableProfilesSheetState extends State<ReorderableProfilesSheet> {
|
||||
late List<Profile> profiles;
|
||||
|
||||
@override
|
||||
@@ -453,74 +460,61 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ReorderableListView.builder(
|
||||
buildDefaultDragHandles: false,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
proxyDecorator: proxyDecorator,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final profile = profiles.removeAt(oldIndex);
|
||||
profiles.insert(newIndex, profile);
|
||||
});
|
||||
},
|
||||
itemBuilder: (_, index) {
|
||||
final profile = profiles[index];
|
||||
return Container(
|
||||
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),
|
||||
),
|
||||
return AdaptiveSheetScaffold(
|
||||
type: widget.type,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
globalState.appController.setProfiles(profiles);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.save,
|
||||
),
|
||||
)
|
||||
],
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(bottom: 32),
|
||||
child: ReorderableListView.builder(
|
||||
buildDefaultDragHandles: false,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
proxyDecorator: proxyDecorator,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final profile = profiles.removeAt(oldIndex);
|
||||
profiles.insert(newIndex, profile);
|
||||
});
|
||||
},
|
||||
itemBuilder: (_, index) {
|
||||
final profile = profiles[index];
|
||||
return Container(
|
||||
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,
|
||||
children: [
|
||||
Text(
|
||||
appLocalizations.confirm,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: profiles.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
title: appLocalizations.profilesSort,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class ProxyCard extends StatelessWidget {
|
||||
delay > 0 ? '$delay ms' : "Timeout",
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: other.getDelayColor(
|
||||
color: utils.getDelayColor(
|
||||
delay,
|
||||
),
|
||||
),
|
||||
@@ -178,7 +178,7 @@ class ProxyCard extends StatelessWidget {
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color:
|
||||
context.textTheme.bodySmall?.color?.toLight,
|
||||
context.textTheme.bodySmall?.color?.opacity80,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -221,7 +221,7 @@ class _ProxyDesc extends ConsumerWidget {
|
||||
desc,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.textTheme.bodySmall?.color?.toLight,
|
||||
color: context.textTheme.bodySmall?.color?.opacity80,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -467,10 +467,6 @@ class _ListHeaderState extends State<ListHeader>
|
||||
return CommonCard(
|
||||
enterAnimated: widget.enterAnimated,
|
||||
key: widget.key,
|
||||
borderSide: WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
context.colorScheme.surfaceContainer,
|
||||
),
|
||||
radius: 14,
|
||||
type: CommonCardType.filled,
|
||||
child: Padding(
|
||||
@@ -556,6 +552,7 @@ class _ListHeaderState extends State<ListHeader>
|
||||
children: [
|
||||
if (isExpand) ...[
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.standard,
|
||||
onPressed: () {
|
||||
widget.onScrollToSelected(groupName);
|
||||
},
|
||||
@@ -565,12 +562,13 @@ class _ListHeaderState extends State<ListHeader>
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _delayTest,
|
||||
visualDensity: VisualDensity.standard,
|
||||
icon: const Icon(
|
||||
Icons.network_ping,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
width: 6,
|
||||
),
|
||||
],
|
||||
AnimatedBuilder(
|
||||
|
||||
@@ -13,8 +13,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
typedef UpdatingMap = Map<String, bool>;
|
||||
|
||||
class ProvidersView extends ConsumerStatefulWidget {
|
||||
final SheetType type;
|
||||
|
||||
const ProvidersView({
|
||||
super.key,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -22,25 +25,6 @@ class ProvidersView extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
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 {
|
||||
final providers = ref.read(providersProvider);
|
||||
@@ -102,10 +86,24 @@ class _ProvidersViewState extends ConsumerState<ProvidersView> {
|
||||
title: appLocalizations.ruleProviders,
|
||||
items: ruleProviders,
|
||||
);
|
||||
return generateListView([
|
||||
...proxySection,
|
||||
...ruleSection,
|
||||
]);
|
||||
return AdaptiveSheetScaffold(
|
||||
actions: [
|
||||
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(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: FadeBox(
|
||||
child: FadeThroughBox(
|
||||
child: provider.isUpdating
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
|
||||
@@ -28,12 +28,13 @@ class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
|
||||
if (_hasProviders)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showExtendPage(
|
||||
isScaffold: true,
|
||||
extendPageWidth: 360,
|
||||
showExtend(
|
||||
context,
|
||||
body: const ProvidersView(),
|
||||
title: appLocalizations.providers,
|
||||
builder: (_, type) {
|
||||
return ProvidersView(
|
||||
type: type,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
@@ -51,11 +52,15 @@ class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
showExtendPage(
|
||||
showExtend(
|
||||
context,
|
||||
extendPageWidth: 360,
|
||||
title: appLocalizations.iconConfiguration,
|
||||
body: _IconConfigView(),
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: const _IconConfigView(),
|
||||
title: appLocalizations.iconConfiguration,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
@@ -65,9 +70,17 @@ class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
title: appLocalizations.proxiesSetting,
|
||||
context: context,
|
||||
body: const ProxiesSetting(),
|
||||
props: SheetProps(
|
||||
isScrollControlled: true,
|
||||
),
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: const ProxiesSetting(),
|
||||
title: appLocalizations.proxiesSetting,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
@@ -128,13 +141,11 @@ class _IconConfigView extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final iconMap =
|
||||
ref.watch(proxiesStyleSettingProvider.select((state) => state.iconMap));
|
||||
final entries = iconMap.entries.toList();
|
||||
return ListPage(
|
||||
return MapInputPage(
|
||||
title: appLocalizations.iconConfiguration,
|
||||
items: entries,
|
||||
map: iconMap,
|
||||
keyLabel: appLocalizations.regExp,
|
||||
valueLabel: appLocalizations.icon,
|
||||
keyBuilder: (item) => Key(item.key),
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
leadingBuilder: (item) => Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -151,10 +162,10 @@ class _IconConfigView extends ConsumerWidget {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onChange: (entries) {
|
||||
onChange: (value) {
|
||||
ref.read(proxiesStyleSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
iconMap: Map.fromEntries(entries),
|
||||
iconMap: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -248,8 +248,8 @@ class ProxiesSetting extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -268,6 +268,7 @@ class ProxiesSetting extends StatelessWidget {
|
||||
return Container();
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._buildGroupStyleSetting(),
|
||||
|
||||
@@ -49,7 +49,7 @@ class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
|
||||
_buildMoreButton() {
|
||||
return Consumer(
|
||||
builder: (_, ref, ___) {
|
||||
final isMobileView = ref.watch(viewWidthProvider.notifier).isMobileView;
|
||||
final isMobileView = ref.watch(isMobileViewProvider);
|
||||
return IconButton(
|
||||
onPressed: _showMoreMenu,
|
||||
icon: isMobileView
|
||||
@@ -67,42 +67,48 @@ class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
|
||||
_showMoreMenu() {
|
||||
showSheet(
|
||||
context: context,
|
||||
width: 380,
|
||||
isScrollControlled: false,
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final state = ref.watch(proxiesSelectorStateProvider);
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final groupName in state.groupNames)
|
||||
SettingTextCard(
|
||||
groupName,
|
||||
onPressed: () {
|
||||
final index = state.groupNames.indexWhere(
|
||||
(item) => item == groupName,
|
||||
);
|
||||
if (index == -1) return;
|
||||
_tabController?.animateTo(index);
|
||||
globalState.appController
|
||||
.updateCurrentGroupName(groupName);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
isSelected: groupName == state.currentGroupName,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
props: SheetProps(
|
||||
isScrollControlled: false,
|
||||
),
|
||||
title: appLocalizations.proxyGroup,
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final state = ref.watch(proxiesSelectorStateProvider);
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final groupName in state.groupNames)
|
||||
SettingTextCard(
|
||||
groupName,
|
||||
onPressed: () {
|
||||
final index = state.groupNames.indexWhere(
|
||||
(item) => item == groupName,
|
||||
);
|
||||
if (index == -1) return;
|
||||
_tabController?.animateTo(index);
|
||||
globalState.appController
|
||||
.updateCurrentGroupName(groupName);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
isSelected: groupName == state.currentGroupName,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
title: appLocalizations.proxyGroup,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -238,7 +244,7 @@ class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
context.colorScheme.surface.withOpacity(0.1),
|
||||
context.colorScheme.surface.opacity10,
|
||||
context.colorScheme.surface,
|
||||
],
|
||||
stops: const [
|
||||
@@ -319,32 +325,35 @@ class ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
|
||||
);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GridView.builder(
|
||||
child: CommonAutoHiddenScrollBar(
|
||||
controller: _controller,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 96,
|
||||
child: GridView.builder(
|
||||
controller: _controller,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 96,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: getItemHeight(proxyCardType),
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
testUrl: state.testUrl,
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
},
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: getItemHeight(proxyCardType),
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
testUrl: state.testUrl,
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
builder: (_, snapshot) {
|
||||
return SizedBox(
|
||||
height: 24,
|
||||
child: FadeBox(
|
||||
child: FadeThroughBox(
|
||||
key: Key("fade_box_${geoItem.label}"),
|
||||
child: snapshot.data == null
|
||||
? const SizedBox(
|
||||
@@ -248,7 +248,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: isUpdating,
|
||||
builder: (_, isUpdating, ___) {
|
||||
return FadeBox(
|
||||
return FadeThroughBox(
|
||||
child: isUpdating
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
@@ -299,24 +299,8 @@ class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.title),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return CommonDialog(
|
||||
title: widget.title,
|
||||
actions: [
|
||||
if (widget.defaultValue != null &&
|
||||
urlController.value.text != widget.defaultValue) ...[
|
||||
@@ -333,6 +317,19 @@ class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
|
||||
child: Text(appLocalizations.submit),
|
||||
)
|
||||
],
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user