Compare commits
16 Commits
v0.8.76
...
v0.8.82-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66f61e4dfa | ||
|
|
a77b3a35e8 | ||
|
|
2d2708d7bd | ||
|
|
ef5f6dbd59 | ||
|
|
b6c7b15e3e | ||
|
|
de9c5ba9cc | ||
|
|
2aae00cf68 | ||
|
|
68be2d34a1 | ||
|
|
7895ccf720 | ||
|
|
e92900dbbd | ||
|
|
eada271c49 | ||
|
|
5dda2854be | ||
|
|
5184ed6fc7 | ||
|
|
4e679f776e | ||
|
|
96328f66e9 | ||
|
|
3eb14ab8a1 |
57
.github/workflows/build.yaml
vendored
57
.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,26 +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:
|
||||
flutter-version: 3.24.5
|
||||
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
|
||||
@@ -89,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)
|
||||
@@ -128,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
|
||||
@@ -207,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: |
|
||||
@@ -215,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 }}
|
||||
@@ -239,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/
|
||||
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,8 +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
|
||||
|
||||
|
||||
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -1,3 +1,77 @@
|
||||
## v0.8.81
|
||||
|
||||
- Add rule override
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.80
|
||||
|
||||
- Optimize dashboard performance
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Fix unselected proxy group delay issues
|
||||
|
||||
- Fix asn url issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.79
|
||||
|
||||
- Fix tab delay view issues
|
||||
|
||||
- Fix tray action issues
|
||||
|
||||
- Fix get profile redirect client ua issues
|
||||
|
||||
- Fix proxy card delay view issues
|
||||
|
||||
- Add Russian, Japanese adaptation
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.78
|
||||
|
||||
- Fix list form input view issues
|
||||
|
||||
- Fix traffic view issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.77
|
||||
|
||||
- Optimize performance
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize core stability
|
||||
|
||||
- Fix linux tun authority check error
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Fix scroll physics error
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.75
|
||||
|
||||
- Add windows storage corruption detection
|
||||
|
||||
- Fix core crash caused by windows resource manager restart
|
||||
|
||||
- Optimize logs, requests, access to pages
|
||||
|
||||
- Fix macos bypass domain issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.74
|
||||
|
||||
- Fix some issues
|
||||
|
||||
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,29 +1 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ enum class AccessControlMode {
|
||||
}
|
||||
|
||||
data class AccessControl(
|
||||
val enable: Boolean,
|
||||
val mode: AccessControlMode,
|
||||
val acceptList: List<String>,
|
||||
val rejectList: List<String>,
|
||||
@@ -17,7 +18,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
|
||||
data class VpnOptions(
|
||||
val enable: Boolean,
|
||||
val port: Int,
|
||||
val accessControl: AccessControl?,
|
||||
val accessControl: AccessControl,
|
||||
val allowBypass: Boolean,
|
||||
val systemProxy: Boolean,
|
||||
val bypassDomain: List<String>,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,9 @@ 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
|
||||
@@ -40,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>()
|
||||
@@ -51,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()
|
||||
@@ -60,6 +62,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
isBind = false
|
||||
flClashService = null
|
||||
}
|
||||
}
|
||||
@@ -90,60 +93,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("fd")
|
||||
if (fd != null) {
|
||||
if (flClashService is FlClashVpnService) {
|
||||
(flClashService as FlClashVpnService).protect(fd)
|
||||
}
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
"resolverProcess" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val process = if (data != null) Gson().fromJson(
|
||||
data, Process::class.java
|
||||
) else null
|
||||
val metadata = process?.metadata
|
||||
if (metadata == null) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
val protocol = metadata.getProtocol()
|
||||
if (protocol == null) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
|
||||
val dst = InetSocketAddress(
|
||||
metadata.destinationIP.ifEmpty { metadata.host },
|
||||
metadata.destinationPort
|
||||
)
|
||||
val uid = try {
|
||||
connectivity?.getConnectionOwnerUid(protocol, src, dst)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (uid == null || uid == -1) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val packages =
|
||||
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
|
||||
result.success(packages?.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
@@ -151,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()
|
||||
@@ -160,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()
|
||||
?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestGc() {
|
||||
@@ -233,6 +184,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun startForegroundJob() {
|
||||
stopForegroundJob()
|
||||
timerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (isActive) {
|
||||
startForeground()
|
||||
@@ -254,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)
|
||||
@@ -68,17 +82,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
addDnsServer(options.dnsServerAddress)
|
||||
setMtu(9000)
|
||||
options.accessControl?.let { accessControl ->
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
options.accessControl.let { accessControl ->
|
||||
if (accessControl.enable) {
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,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)
|
||||
@@ -180,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.
Binary file not shown.
@@ -5,6 +5,7 @@ targets:
|
||||
options:
|
||||
build_extensions:
|
||||
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
|
||||
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
|
||||
freezed:
|
||||
options:
|
||||
build_extensions:
|
||||
|
||||
Submodule core/Clash.Meta updated: 0c03d8e4b4...f19dad529f
@@ -22,99 +22,99 @@ func (result ActionResult) Json() ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (action Action) wrapMessage(data interface{}) []byte {
|
||||
sendAction := ActionResult{
|
||||
func (action Action) getResult(data interface{}) []byte {
|
||||
resultAction := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
Data: data,
|
||||
}
|
||||
res, _ := sendAction.Json()
|
||||
res, _ := resultAction.Json()
|
||||
return res
|
||||
}
|
||||
|
||||
func handleAction(action *Action, send func([]byte)) {
|
||||
func handleAction(action *Action, result func(data interface{})) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
data := action.Data.(string)
|
||||
send(action.wrapMessage(handleInitClash(data)))
|
||||
paramsString := action.Data.(string)
|
||||
result(handleInitClash(paramsString))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
send(action.wrapMessage(handleGetIsInit()))
|
||||
result(handleGetIsInit())
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case shutdownMethod:
|
||||
send(action.wrapMessage(handleShutdown()))
|
||||
result(handleShutdown())
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleValidateConfig(data)))
|
||||
result(handleValidateConfig(data))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleUpdateConfig(data)))
|
||||
result(handleUpdateConfig(data))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
send(action.wrapMessage(handleGetProxies()))
|
||||
result(handleGetProxies())
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTraffic()))
|
||||
result(handleGetTraffic())
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTotalTraffic()))
|
||||
result(handleGetTotalTraffic())
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
send(action.wrapMessage(handleGetConnections()))
|
||||
result(handleGetConnections())
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
send(action.wrapMessage(handleCloseConnections()))
|
||||
result(handleCloseConnections())
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
send(action.wrapMessage(handleCloseConnection(id)))
|
||||
result(handleCloseConnection(id))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
send(action.wrapMessage(handleGetExternalProviders()))
|
||||
result(handleGetExternalProviders())
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
send(action.wrapMessage(handleGetExternalProvider(externalProviderName)))
|
||||
result(handleGetExternalProvider(externalProviderName))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
result(err.Error())
|
||||
return
|
||||
}
|
||||
geoType := params["geo-type"]
|
||||
geoName := params["geo-name"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
@@ -122,46 +122,56 @@ func handleAction(action *Action, send func([]byte)) {
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
result(err.Error())
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case startListenerMethod:
|
||||
send(action.wrapMessage(handleStartListener()))
|
||||
result(handleStartListener())
|
||||
return
|
||||
case stopListenerMethod:
|
||||
send(action.wrapMessage(handleStopListener()))
|
||||
result(handleStopListener())
|
||||
return
|
||||
case getCountryCodeMethod:
|
||||
ip := action.Data.(string)
|
||||
handleGetCountryCode(ip, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getMemoryMethod:
|
||||
handleGetMemory(func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getProfileMethod:
|
||||
profileId := action.Data.(string)
|
||||
handleGetMemory(func(value string) {
|
||||
result(handleGetProfile(profileId))
|
||||
})
|
||||
return
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
result(true)
|
||||
default:
|
||||
handle := nextHandle(action, send)
|
||||
handle := nextHandle(action, result)
|
||||
if handle {
|
||||
return
|
||||
} else {
|
||||
send(action.wrapMessage(action.DefaultValue))
|
||||
result(action.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,10 +28,23 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func splitByMultipleSeparators(s string) interface{} {
|
||||
isSeparator := func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == ';'
|
||||
}
|
||||
|
||||
parts := strings.FieldsFunc(s, isSeparator)
|
||||
if len(parts) > 1 {
|
||||
return parts
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
version = 0
|
||||
isRunning = false
|
||||
runLock sync.Mutex
|
||||
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
|
||||
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
|
||||
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||
)
|
||||
|
||||
@@ -160,9 +173,19 @@ 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 {
|
||||
hosts[k] = v
|
||||
if str, ok := v.(string); ok {
|
||||
hosts[k] = splitByMultipleSeparators(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePatchDns(dns config.RawDNS) {
|
||||
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
|
||||
if str, ok := pair.Value.(string); ok {
|
||||
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,26 +196,25 @@ func trimArr(arr []string) (r []string) {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -215,6 +237,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.Tun.Device = patchConfig.Tun.Device
|
||||
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
||||
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
||||
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
|
||||
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
||||
targetConfig.Profile.StoreSelected = false
|
||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||
@@ -225,15 +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 {
|
||||
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,13 +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"`
|
||||
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||
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 {
|
||||
@@ -71,15 +76,12 @@ const (
|
||||
stopLogMethod Method = "stopLog"
|
||||
startListenerMethod Method = "startListener"
|
||||
stopListenerMethod Method = "stopListener"
|
||||
startTunMethod Method = "startTun"
|
||||
stopTunMethod Method = "stopTun"
|
||||
updateDnsMethod Method = "updateDns"
|
||||
setProcessMapMethod Method = "setProcessMap"
|
||||
setFdMapMethod Method = "setFdMap"
|
||||
setStateMethod Method = "setState"
|
||||
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||
getRunTimeMethod Method = "getRunTime"
|
||||
getCurrentProfileNameMethod Method = "getCurrentProfileName"
|
||||
getProfileMethod Method = "getProfile"
|
||||
)
|
||||
|
||||
type Method string
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
48
core/go.mod
48
core/go.mod
@@ -6,7 +6,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require (
|
||||
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/samber/lo v1.49.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -19,28 +19,28 @@ require (
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/enfein/mieru/v3 v3.10.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.13.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.1 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
@@ -50,22 +50,24 @@ 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.0 // 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/quic-go v0.48.3-0.20241126053724-b69fea3888da // 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/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // 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-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // 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.62 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
@@ -73,18 +75,18 @@ require (
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.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
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
github.com/sagernet/sing v0.5.1 // indirect
|
||||
github.com/sagernet/sing v0.5.2 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
|
||||
@@ -101,13 +103,13 @@ require (
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
|
||||
102
core/go.sum
102
core/go.sum
@@ -24,12 +24,12 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
|
||||
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
@@ -43,8 +43,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
|
||||
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
|
||||
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
|
||||
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
@@ -74,8 +74,8 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
@@ -84,6 +84,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||
@@ -96,40 +97,45 @@ 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.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
|
||||
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
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/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30=
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
|
||||
github.com/metacubex/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=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
@@ -149,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.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.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=
|
||||
@@ -164,18 +170,18 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJ
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
|
||||
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
|
||||
@@ -218,8 +224,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
@@ -228,11 +234,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -248,12 +254,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
@@ -263,8 +269,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
35
core/hub.go
35
core/hub.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"core/state"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
@@ -26,18 +27,22 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{
|
||||
OnlyStatisticsProxy: false,
|
||||
}
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{}
|
||||
externalProviders = map[string]cp.Provider{}
|
||||
logSubscriber observable.Subscription[log.Event]
|
||||
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
|
||||
@@ -152,7 +157,7 @@ func handleChangeProxy(data string, fn func(string string)) {
|
||||
}
|
||||
|
||||
func handleGetTraffic() string {
|
||||
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy)
|
||||
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -166,7 +171,7 @@ func handleGetTraffic() string {
|
||||
}
|
||||
|
||||
func handleGetTotalTraffic() string {
|
||||
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy)
|
||||
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -179,6 +184,15 @@ func handleGetTotalTraffic() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleGetProfile(profileId string) string {
|
||||
prof := getRawConfigWithId(profileId)
|
||||
data, err := json.Marshal(prof)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleResetTraffic() {
|
||||
statistic.DefaultManager.ResetStatistic()
|
||||
}
|
||||
@@ -220,6 +234,7 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
||||
if params.TestUrl != "" {
|
||||
testUrl = params.TestUrl
|
||||
}
|
||||
delayData.Url = testUrl
|
||||
|
||||
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
|
||||
if err != nil || delay == 0 {
|
||||
@@ -423,6 +438,10 @@ func handleGetMemory(fn func(value string)) {
|
||||
}()
|
||||
}
|
||||
|
||||
func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func init() {
|
||||
adapter.UrlTestHook = func(url string, name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
|
||||
@@ -49,8 +49,8 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
bridge.SendToPort(i, string(bytes))
|
||||
go handleAction(action, func(data interface{}) {
|
||||
bridge.SendToPort(i, string(action.getResult(data)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func sendMessage(message Message) {
|
||||
}
|
||||
bridge.SendToPort(messagePort, string(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res)))
|
||||
}.getResult(res)))
|
||||
}
|
||||
|
||||
//export startListener
|
||||
|
||||
@@ -4,6 +4,7 @@ package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
bridge "core/dart-bridge"
|
||||
"core/platform"
|
||||
"core/state"
|
||||
@@ -11,128 +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()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InvokeManager) load(id string) string {
|
||||
res, ok := m.invokeMap.Load(id)
|
||||
if ok {
|
||||
return res.(string)
|
||||
if t.callback != nil {
|
||||
releaseObject(t.callback)
|
||||
}
|
||||
return ""
|
||||
t.callback = nil
|
||||
t.listener = nil
|
||||
}
|
||||
|
||||
func (m *InvokeManager) delete(id string) {
|
||||
m.invokeMap.Delete(id)
|
||||
}
|
||||
func (t *TunHandler) handleProtect(fd int) {
|
||||
_ = t.limit.Acquire(context.Background(), 1)
|
||||
defer t.limit.Release(1)
|
||||
|
||||
func (m *InvokeManager) completer(id string, value string) {
|
||||
m.invokeMap.Store(id, value)
|
||||
m.chanLock.Lock()
|
||||
if ch, ok := m.chanMap[id]; ok {
|
||||
close(ch)
|
||||
delete(m.chanMap, id)
|
||||
}
|
||||
m.chanLock.Unlock()
|
||||
}
|
||||
|
||||
func (m *InvokeManager) await(id string) {
|
||||
m.chanLock.Lock()
|
||||
if _, ok := m.chanMap[id]; !ok {
|
||||
m.chanMap[id] = make(chan struct{})
|
||||
}
|
||||
ch := m.chanMap[id]
|
||||
m.chanLock.Unlock()
|
||||
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
case <-timeout:
|
||||
m.completer(id, "")
|
||||
if t.listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
protect(t.callback, fd)
|
||||
}
|
||||
|
||||
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
|
||||
_ = t.limit.Acquire(context.Background(), 1)
|
||||
defer t.limit.Release(1)
|
||||
|
||||
if t.listener == nil {
|
||||
return ""
|
||||
}
|
||||
var protocol int
|
||||
uid := -1
|
||||
switch source.Network() {
|
||||
case "udp", "udp4", "udp6":
|
||||
protocol = syscall.IPPROTO_UDP
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
protocol = syscall.IPPROTO_TCP
|
||||
}
|
||||
if version < 29 {
|
||||
uid = platform.QuerySocketUidFromProcFs(source, target)
|
||||
}
|
||||
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
|
||||
}
|
||||
|
||||
var (
|
||||
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 ""
|
||||
@@ -140,99 +129,41 @@ 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)
|
||||
fdInvokeMap.delete(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,
|
||||
})
|
||||
processInvokeMap.await(id)
|
||||
res := processInvokeMap.load(id)
|
||||
processInvokeMap.delete(id)
|
||||
return res, nil
|
||||
return tunHandler.handleResolveProcess(src, dst), nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeTunHook() {
|
||||
dialer.DefaultSocketHook = nil
|
||||
process.DefaultPackageNameResolver = nil
|
||||
}
|
||||
|
||||
func handleGetAndroidVpnOptions() string {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
options := state.AndroidVpnOptions{
|
||||
Enable: state.CurrentState.Enable,
|
||||
Enable: state.CurrentState.VpnProps.Enable,
|
||||
Port: currentConfig.General.MixedPort,
|
||||
Ipv4Address: state.DefaultIpv4Address,
|
||||
Ipv6Address: state.GetIpv6Address(),
|
||||
AccessControl: state.CurrentState.AccessControl,
|
||||
SystemProxy: state.CurrentState.SystemProxy,
|
||||
AllowBypass: state.CurrentState.AllowBypass,
|
||||
RouteAddress: state.CurrentState.RouteAddress,
|
||||
AccessControl: state.CurrentState.VpnProps.AccessControl,
|
||||
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
|
||||
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
|
||||
RouteAddress: currentConfig.General.Tun.RouteAddress,
|
||||
BypassDomain: state.CurrentState.BypassDomain,
|
||||
DnsServerAddress: state.GetDnsServerAddress(),
|
||||
}
|
||||
@@ -244,10 +175,6 @@ func handleGetAndroidVpnOptions() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func handleUpdateDns(value string) {
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", value)
|
||||
@@ -263,59 +190,34 @@ func handleGetCurrentProfileName() string {
|
||||
return state.CurrentState.CurrentProfileName
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
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)
|
||||
send(action.wrapMessage(handleStartTun(fd)))
|
||||
return true
|
||||
case stopTunMethod:
|
||||
handleStopTun()
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case getAndroidVpnOptionsMethod:
|
||||
send(action.wrapMessage(handleGetAndroidVpnOptions()))
|
||||
result(handleGetAndroidVpnOptions())
|
||||
return true
|
||||
case updateDnsMethod:
|
||||
data := action.Data.(string)
|
||||
handleUpdateDns(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setFdMapMethod:
|
||||
fdId := action.Data.(string)
|
||||
handleSetFdMap(fdId)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setProcessMapMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetProcessMap(data)
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return true
|
||||
case getRunTimeMethod:
|
||||
send(action.wrapMessage(handleGetRunTime()))
|
||||
result(handleGetRunTime())
|
||||
return true
|
||||
case getCurrentProfileNameMethod:
|
||||
send(action.wrapMessage(handleGetCurrentProfileName()))
|
||||
result(handleGetCurrentProfileName())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//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")
|
||||
}
|
||||
@@ -325,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
|
||||
@@ -340,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())
|
||||
@@ -367,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)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
package main
|
||||
|
||||
func nextHandle(action *Action) {
|
||||
return action
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func sendMessage(message Message) {
|
||||
}
|
||||
send(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res))
|
||||
}.getResult(res))
|
||||
}
|
||||
|
||||
func send(data []byte) {
|
||||
@@ -61,12 +61,12 @@ func startServer(arg string) {
|
||||
return
|
||||
}
|
||||
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
send(bytes)
|
||||
go handleAction(action, func(data interface{}) {
|
||||
send(action.getResult(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//go:build android && cgo
|
||||
|
||||
package state
|
||||
|
||||
import "net/netip"
|
||||
|
||||
var DefaultIpv4Address = "172.19.0.1/30"
|
||||
var DefaultDnsAddress = "172.19.0.2"
|
||||
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
|
||||
@@ -13,13 +13,14 @@ type AndroidVpnOptions struct {
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
BypassDomain []string `json:"bypassDomain"`
|
||||
RouteAddress []string `json:"routeAddress"`
|
||||
RouteAddress []netip.Prefix `json:"routeAddress"`
|
||||
Ipv4Address string `json:"ipv4Address"`
|
||||
Ipv6Address string `json:"ipv6Address"`
|
||||
DnsServerAddress string `json:"dnsServerAddress"`
|
||||
}
|
||||
|
||||
type AccessControl struct {
|
||||
Enable bool `json:"enable"`
|
||||
Mode string `json:"mode"`
|
||||
AcceptList []string `json:"acceptList"`
|
||||
RejectList []string `json:"rejectList"`
|
||||
@@ -31,20 +32,23 @@ type AndroidVpnRawOptions struct {
|
||||
AccessControl *AccessControl `json:"accessControl"`
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
RouteAddress []string `json:"routeAddress"`
|
||||
Ipv6 bool `json:"ipv6"`
|
||||
BypassDomain []string `json:"bypassDomain"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
AndroidVpnRawOptions
|
||||
CurrentProfileName string `json:"currentProfileName"`
|
||||
VpnProps AndroidVpnRawOptions `json:"vpn-props"`
|
||||
CurrentProfileName string `json:"current-profile-name"`
|
||||
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||
BypassDomain []string `json:"bypass-domain"`
|
||||
}
|
||||
|
||||
var CurrentState = &State{}
|
||||
var CurrentState = &State{
|
||||
OnlyStatisticsProxy: false,
|
||||
CurrentProfileName: "",
|
||||
}
|
||||
|
||||
func GetIpv6Address() string {
|
||||
if CurrentState.Ipv6 {
|
||||
if CurrentState.VpnProps.Ipv6 {
|
||||
return DefaultIpv6Address
|
||||
} else {
|
||||
return ""
|
||||
|
||||
@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
|
||||
}
|
||||
prefix4 = append(prefix4, tempPrefix4)
|
||||
var prefix6 []netip.Prefix
|
||||
if state.CurrentState.Ipv6 {
|
||||
if state.CurrentState.VpnProps.Ipv6 {
|
||||
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
|
||||
@@ -7,57 +7,27 @@ 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/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'models/models.dart';
|
||||
import 'pages/pages.dart';
|
||||
|
||||
runAppWithPreferences(
|
||||
Widget child, {
|
||||
required AppState appState,
|
||||
required Config config,
|
||||
required AppFlowingState appFlowingState,
|
||||
required ClashConfig clashConfig,
|
||||
}) {
|
||||
runApp(MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ClashConfig>(
|
||||
create: (_) => clashConfig,
|
||||
),
|
||||
ChangeNotifierProvider<Config>(
|
||||
create: (_) => config,
|
||||
),
|
||||
ChangeNotifierProvider<AppFlowingState>(
|
||||
create: (_) => appFlowingState,
|
||||
),
|
||||
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
||||
create: (_) => appState,
|
||||
update: (_, config, clashConfig, appState) {
|
||||
appState?.mode = clashConfig.mode;
|
||||
appState?.selectedMap = config.currentSelectedMap;
|
||||
return appState!;
|
||||
},
|
||||
)
|
||||
],
|
||||
child: child,
|
||||
));
|
||||
}
|
||||
|
||||
class Application extends StatefulWidget {
|
||||
class Application extends ConsumerStatefulWidget {
|
||||
const Application({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Application> createState() => ApplicationState();
|
||||
ConsumerState<Application> createState() => ApplicationState();
|
||||
}
|
||||
|
||||
class ApplicationState extends State<Application> {
|
||||
late SystemColorSchemes systemColorSchemes;
|
||||
class ApplicationState extends ConsumerState<Application> {
|
||||
late ColorSchemes systemColorSchemes;
|
||||
Timer? _autoUpdateGroupTaskTimer;
|
||||
Timer? _autoUpdateProfilesTaskTimer;
|
||||
|
||||
@@ -73,16 +43,8 @@ class ApplicationState extends State<Application> {
|
||||
ColorScheme _getAppColorScheme({
|
||||
required Brightness brightness,
|
||||
int? primaryColor,
|
||||
required SystemColorSchemes systemColorSchemes,
|
||||
}) {
|
||||
if (primaryColor != null) {
|
||||
return ColorScheme.fromSeed(
|
||||
seedColor: Color(primaryColor),
|
||||
brightness: brightness,
|
||||
);
|
||||
} else {
|
||||
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
|
||||
}
|
||||
return ref.read(genColorSchemeProvider(brightness));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -90,12 +52,11 @@ class ApplicationState extends State<Application> {
|
||||
super.initState();
|
||||
_autoUpdateGroupTask();
|
||||
_autoUpdateProfilesTask();
|
||||
globalState.appController = AppController(context);
|
||||
globalState.measure = Measure.of(context);
|
||||
globalState.appController = AppController(context, ref);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final currentContext = globalState.navigatorKey.currentContext;
|
||||
if (currentContext != null) {
|
||||
globalState.appController = AppController(currentContext);
|
||||
globalState.appController = AppController(currentContext, ref);
|
||||
}
|
||||
await globalState.appController.init();
|
||||
globalState.appController.initLink();
|
||||
@@ -119,7 +80,7 @@ class ApplicationState extends State<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_buildPlatformWrap(Widget child) {
|
||||
_buildPlatformState(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowManager(
|
||||
child: TrayManager(
|
||||
@@ -138,18 +99,7 @@ class ApplicationState extends State<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(
|
||||
@@ -163,11 +113,30 @@ class ApplicationState extends State<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_buildPlatformApp(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return VpnManager(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
_buildApp(Widget child) {
|
||||
return MessageManager(
|
||||
child: ThemeManager(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_updateSystemColorSchemes(
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
) {
|
||||
systemColorSchemes = SystemColorSchemes(
|
||||
systemColorSchemes = ColorSchemes(
|
||||
lightColorScheme: lightDynamic,
|
||||
darkColorScheme: darkDynamic,
|
||||
);
|
||||
@@ -178,21 +147,18 @@ class ApplicationState extends State<Application> {
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildPlatformWrap(
|
||||
_buildWrap(
|
||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.appSetting.locale,
|
||||
themeMode: config.themeProps.themeMode,
|
||||
primaryColor: config.themeProps.primaryColor,
|
||||
prueBlack: config.themeProps.prueBlack,
|
||||
fontFamily: config.themeProps.fontFamily,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
return _buildPlatformState(
|
||||
_buildState(
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final locale =
|
||||
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||
final themeProps = ref.watch(themeSettingProvider);
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
@@ -201,43 +167,32 @@ class ApplicationState extends State<Application> {
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return MessageManager(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
final appController = globalState.appController;
|
||||
final maxWidth = container.maxWidth;
|
||||
if (appController.appState.viewWidth != maxWidth) {
|
||||
globalState.appController.updateViewWidth(maxWidth);
|
||||
}
|
||||
return _buildPage(child!);
|
||||
},
|
||||
return AppEnvManager(
|
||||
child: _buildPlatformApp(
|
||||
_buildApp(child!),
|
||||
),
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(state.locale),
|
||||
locale: utils.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: state.themeMode,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
).toPrueBlack(state.prueBlack),
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPureBlack(themeProps.pureBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -63,13 +64,19 @@ class ClashCore {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> init({
|
||||
required ClashConfig clashConfig,
|
||||
required Config config,
|
||||
}) async {
|
||||
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 {
|
||||
return await clashInterface.setState(state);
|
||||
}
|
||||
|
||||
shutdown() async {
|
||||
@@ -234,6 +241,14 @@ class ClashCore {
|
||||
return int.parse(value);
|
||||
}
|
||||
|
||||
Future<ClashConfigSnippet?> getProfile(String id) async {
|
||||
final res = await clashInterface.getProfile(id);
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
|
||||
}
|
||||
|
||||
resetTraffic() {
|
||||
clashInterface.resetTraffic();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -2,15 +2,12 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/clash/message.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/common/future.dart';
|
||||
import 'package:fl_clash/common/other.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
|
||||
mixin ClashInterface {
|
||||
Future<bool> init(String homeDir);
|
||||
Future<bool> init(InitParams params);
|
||||
|
||||
Future<bool> preload();
|
||||
|
||||
@@ -66,6 +63,10 @@ mixin ClashInterface {
|
||||
FutureOr<bool> closeConnection(String id);
|
||||
|
||||
FutureOr<bool> closeConnections();
|
||||
|
||||
FutureOr<String> getProfile(String id);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
}
|
||||
|
||||
mixin AndroidClashInterface {
|
||||
@@ -73,14 +74,10 @@ mixin AndroidClashInterface {
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
|
||||
Future<bool> stopTun();
|
||||
// Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
|
||||
Future<DateTime?> startTun(int fd);
|
||||
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||
|
||||
Future<String> getCurrentProfileName();
|
||||
@@ -106,6 +103,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
case ActionMethod.closeConnections:
|
||||
case ActionMethod.closeConnection:
|
||||
case ActionMethod.stopListener:
|
||||
case ActionMethod.setState:
|
||||
completer?.complete(result.data as bool);
|
||||
return;
|
||||
case ActionMethod.changeProxy:
|
||||
@@ -137,7 +135,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
completer?.complete(result.data);
|
||||
}
|
||||
} catch (_) {
|
||||
debugPrint(result.id);
|
||||
commonPrint.log(result.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,18 @@ 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),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setState(CoreState state) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setState,
|
||||
data: json.encode(state),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -232,6 +238,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
return await invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
timeout: Duration(minutes: 2),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,6 +246,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
Future<String> getProxies() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProxies,
|
||||
timeout: Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,6 +326,14 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProfile(String id) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProfile,
|
||||
data: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTotalTraffic() {
|
||||
return invoke<String>(
|
||||
|
||||
@@ -67,7 +67,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
switch (result.method) {
|
||||
case ActionMethod.setFdMap:
|
||||
case ActionMethod.setProcessMap:
|
||||
case ActionMethod.setState:
|
||||
case ActionMethod.stopTun:
|
||||
case ActionMethod.updateDns:
|
||||
completer?.complete(result.data as bool);
|
||||
@@ -123,33 +122,12 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setState(CoreState state) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setState,
|
||||
data: json.encode(state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> startTun(int fd) async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.startTun,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopTun() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopTun,
|
||||
);
|
||||
}
|
||||
// @override
|
||||
// Future<bool> stopTun() {
|
||||
// return invoke<bool>(
|
||||
// method: ActionMethod.stopTun,
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
|
||||
@@ -233,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);
|
||||
@@ -314,14 +267,8 @@ class ClashLibHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
setFdMap(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.setFdMap(idChar);
|
||||
malloc.free(idChar);
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
String homeDir,
|
||||
InitParams initParams,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
@@ -334,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;
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dart:typed_data';
|
||||
import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class ClashService extends ClashHandlerInterface {
|
||||
static ClashService? _instance;
|
||||
@@ -14,6 +15,8 @@ class ClashService extends ClashHandlerInterface {
|
||||
|
||||
Completer<Socket> socketCompleter = Completer();
|
||||
|
||||
bool isStarting = false;
|
||||
|
||||
Process? process;
|
||||
|
||||
factory ClashService() {
|
||||
@@ -27,48 +30,61 @@ class ClashService extends ClashHandlerInterface {
|
||||
}
|
||||
|
||||
_initServer() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
type: InternetAddressType.unix,
|
||||
)
|
||||
: InternetAddress(
|
||||
localhost,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(
|
||||
address,
|
||||
0,
|
||||
shared: true,
|
||||
);
|
||||
serverCompleter.complete(server);
|
||||
await for (final socket in server) {
|
||||
await _destroySocket();
|
||||
socketCompleter.complete(socket);
|
||||
socket
|
||||
.transform(
|
||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
||||
handleData: (Uint8List data, EventSink<String> sink) {
|
||||
sink.add(utf8.decode(data, allowMalformed: true));
|
||||
runZonedGuarded(() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
type: InternetAddressType.unix,
|
||||
)
|
||||
: InternetAddress(
|
||||
localhost,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(
|
||||
address,
|
||||
0,
|
||||
shared: true,
|
||||
);
|
||||
serverCompleter.complete(server);
|
||||
await for (final socket in server) {
|
||||
await _destroySocket();
|
||||
socketCompleter.complete(socket);
|
||||
socket
|
||||
.transform(
|
||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
||||
handleData: (Uint8List data, EventSink<String> sink) {
|
||||
sink.add(utf8.decode(data, allowMalformed: true));
|
||||
},
|
||||
),
|
||||
)
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, (error, stack) {
|
||||
commonPrint.log(error.toString());
|
||||
if(error is SocketException){
|
||||
globalState.showNotifier(error.toString());
|
||||
globalState.appController.restartCore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
reStart() async {
|
||||
if (isStarting == true) {
|
||||
return;
|
||||
}
|
||||
isStarting = true;
|
||||
socketCompleter = Completer();
|
||||
if (process != null) {
|
||||
await shutdown();
|
||||
}
|
||||
@@ -90,6 +106,7 @@ class ClashService extends ClashHandlerInterface {
|
||||
],
|
||||
);
|
||||
process!.stdout.listen((_) {});
|
||||
isStarting = false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -125,7 +142,6 @@ class ClashService extends ClashHandlerInterface {
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
await super.shutdown();
|
||||
if (Platform.isWindows) {
|
||||
await request.stopCoreByHelper();
|
||||
}
|
||||
|
||||
@@ -1,28 +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.12);
|
||||
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(
|
||||
@@ -51,11 +115,11 @@ extension ColorExtension on Color {
|
||||
}
|
||||
|
||||
extension ColorSchemeExtension on ColorScheme {
|
||||
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
|
||||
ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
|
||||
? 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';
|
||||
@@ -35,4 +35,5 @@ export 'tray.dart';
|
||||
export 'window.dart';
|
||||
export 'windows.dart';
|
||||
export 'render.dart';
|
||||
export 'view.dart';
|
||||
export 'mixin.dart';
|
||||
export 'print.dart';
|
||||
@@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
|
||||
const appName = "FlClash";
|
||||
const appHelperService = "FlClashHelperService";
|
||||
const coreName = "clash.meta";
|
||||
const browserUa =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||
const packageName = "com.follow.clash";
|
||||
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
|
||||
const helperPort = 47890;
|
||||
@@ -19,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);
|
||||
@@ -33,16 +40,6 @@ final double kHeaderHeight = system.isDesktop
|
||||
? 40
|
||||
: 28
|
||||
: 0;
|
||||
const GeoXMap defaultGeoXMap = {
|
||||
"mmdb":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||
"asn":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
|
||||
"geoip":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
|
||||
"geosite":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
|
||||
};
|
||||
const profilesDirectoryName = "profiles";
|
||||
const localhost = "127.0.0.1";
|
||||
const clashConfigKey = "clash_config";
|
||||
@@ -53,10 +50,8 @@ const repository = "chen08209/FlClash";
|
||||
const defaultExternalController = "127.0.0.1:9090";
|
||||
const maxMobileWidth = 600;
|
||||
const maxLaptopWidth = 840;
|
||||
const geodataLoaderMemconservative = "memconservative";
|
||||
const geodataLoaderStandard = "standard";
|
||||
const defaultTestUrl = "https://www.gstatic.com/generate_204";
|
||||
final filter = ImageFilter.blur(
|
||||
final commonFilter = ImageFilter.blur(
|
||||
sigmaX: 5,
|
||||
sigmaY: 5,
|
||||
tileMode: TileMode.mirror,
|
||||
@@ -83,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,10 +61,27 @@ class Throttler {
|
||||
|
||||
cancel(dynamic tag) {
|
||||
_operations[tag]?.cancel();
|
||||
_operations[tag] = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> retry<T>({
|
||||
required Future<T> Function() task,
|
||||
int maxAttempts = 3,
|
||||
required bool Function(T res) retryIf,
|
||||
Duration delay = Duration.zero,
|
||||
}) async {
|
||||
int attempts = 0;
|
||||
while (attempts < maxAttempts) {
|
||||
final res = await task();
|
||||
if (!retryIf(res) || attempts >= maxAttempts) {
|
||||
return res;
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
throw "unknown error";
|
||||
}
|
||||
|
||||
final debouncer = Debouncer();
|
||||
|
||||
final throttler = Throttler();
|
||||
final throttler = Throttler();
|
||||
|
||||
@@ -10,7 +10,7 @@ extension CompleterExt<T> on Completer<T> {
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
required String functionName,
|
||||
}) {
|
||||
final realTimeout = timeout ?? const Duration(seconds: 1);
|
||||
final realTimeout = timeout ?? const Duration(seconds: 30);
|
||||
Timer(realTimeout + commonDuration, () {
|
||||
if (onLast != null) {
|
||||
onLast();
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../state.dart';
|
||||
import 'constant.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class FlClashHttpOverrides extends HttpOverrides {
|
||||
static String handleFindProxy(Uri url) {
|
||||
if ([localhost].contains(url.host)) {
|
||||
return "DIRECT";
|
||||
}
|
||||
final port = globalState.config.patchClashConfig.mixedPort;
|
||||
final isStart = globalState.appState.runTime != null;
|
||||
commonPrint.log("find $url proxy:$isStart");
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
}
|
||||
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
final client = super.createHttpClient(context);
|
||||
client.badCertificateCallback = (_, __, ___) => true;
|
||||
client.findProxy = (url) {
|
||||
if ([localhost].contains(url.host)) {
|
||||
return "DIRECT";
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
final port = appController.clashConfig.mixedPort;
|
||||
final isStart = appController.appFlowingState.isStart;
|
||||
debugPrint("find $url proxy:$isStart");
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
};
|
||||
client.findProxy = handleFindProxy;
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'print.dart';
|
||||
|
||||
typedef InstallConfigCallBack = void Function(String url);
|
||||
|
||||
@@ -15,11 +16,11 @@ class LinkManager {
|
||||
}
|
||||
|
||||
initAppLinksListen(installConfigCallBack) async {
|
||||
debugPrint("initAppLinksListen");
|
||||
commonPrint.log("initAppLinksListen");
|
||||
destroy();
|
||||
subscription = _appLinks.uriLinkStream.listen(
|
||||
(uri) {
|
||||
debugPrint('onAppLink: $uri');
|
||||
commonPrint.log('onAppLink: $uri');
|
||||
if (uri.host == 'install-config') {
|
||||
final parameters = uri.queryParameters;
|
||||
final url = parameters['url'];
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'dart:collection';
|
||||
|
||||
class FixedList<T> {
|
||||
final int maxLength;
|
||||
final List<T> _list = [];
|
||||
final List<T> _list;
|
||||
|
||||
FixedList(this.maxLength);
|
||||
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
|
||||
|
||||
add(T item) {
|
||||
if (_list.length == maxLength) {
|
||||
@@ -13,15 +13,26 @@ class FixedList<T> {
|
||||
_list.add(item);
|
||||
}
|
||||
|
||||
clear() {
|
||||
_list.clear();
|
||||
}
|
||||
|
||||
List<T> get list => List.unmodifiable(_list);
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
T operator [](int index) => _list[index];
|
||||
|
||||
FixedList<T> copyWith() {
|
||||
return FixedList(
|
||||
maxLength,
|
||||
list: _list,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FixedMap<K, V> {
|
||||
final int maxSize;
|
||||
int maxSize;
|
||||
final Map<K, V> _map = {};
|
||||
final Queue<K> _queue = Queue<K>();
|
||||
|
||||
@@ -34,15 +45,21 @@ class FixedMap<K, V> {
|
||||
}
|
||||
_map[key] = value;
|
||||
_queue.add(key);
|
||||
return value;
|
||||
}
|
||||
|
||||
clear(){
|
||||
clear() {
|
||||
_map.clear();
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
updateMaxSize(int size){
|
||||
maxSize = size;
|
||||
}
|
||||
|
||||
V? get(K key) => _map[key];
|
||||
|
||||
|
||||
bool containsKey(K key) => _map.containsKey(key);
|
||||
|
||||
int get length => _map.length;
|
||||
|
||||
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class Measure {
|
||||
final TextScaler _textScale;
|
||||
late BuildContext context;
|
||||
final BuildContext context;
|
||||
|
||||
Measure.of(this.context)
|
||||
: _textScale = TextScaler.linear(
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||
textScaleFactor,
|
||||
);
|
||||
|
||||
Size computeTextSize(
|
||||
@@ -16,7 +16,10 @@ class Measure {
|
||||
double maxWidth = double.infinity,
|
||||
}) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: text.data, style: text.style),
|
||||
text: TextSpan(
|
||||
text: text.data,
|
||||
style: text.style,
|
||||
),
|
||||
maxLines: text.maxLines,
|
||||
textScaler: _textScale,
|
||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||
|
||||
53
lib/common/mixin.dart
Normal file
53
lib/common/mixin.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'context.dart';
|
||||
|
||||
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
|
||||
set value(T value) {
|
||||
state = value;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(previous, next) {
|
||||
final res = super.updateShouldNotify(previous, next);
|
||||
if (res) {
|
||||
onUpdate(next);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
onUpdate(T value) {}
|
||||
}
|
||||
|
||||
mixin PageMixin<T extends StatefulWidget> on State<T> {
|
||||
void onPageShow() {
|
||||
initPageState();
|
||||
}
|
||||
|
||||
initPageState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.actions = actions;
|
||||
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||
commonScaffoldState?.updateSearchState(
|
||||
(_) => onSearch != null
|
||||
? AppBarSearchState(
|
||||
onSearch: onSearch!,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void onPageHidden() {}
|
||||
|
||||
List<Widget> get actions => [];
|
||||
|
||||
Widget? get floatingActionButton => null;
|
||||
|
||||
Function(String)? get onSearch => null;
|
||||
|
||||
Function(List<String>)? get onKeywordsUpdate => null;
|
||||
}
|
||||
@@ -6,55 +6,82 @@ import 'package:flutter/material.dart';
|
||||
class Navigation {
|
||||
static Navigation? _instance;
|
||||
|
||||
getItems({
|
||||
List<NavigationItem> getItems({
|
||||
bool openLogs = false,
|
||||
bool hasProxies = false,
|
||||
}) {
|
||||
return [
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.space_dashboard),
|
||||
label: "dashboard",
|
||||
fragment: DashboardFragment(),
|
||||
label: PageLabel.dashboard,
|
||||
keep: false,
|
||||
fragment: DashboardFragment(
|
||||
key: GlobalObjectKey(PageLabel.dashboard),
|
||||
),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.rocket),
|
||||
label: "proxies",
|
||||
fragment: const ProxiesFragment(),
|
||||
icon: const Icon(Icons.article),
|
||||
label: PageLabel.proxies,
|
||||
fragment: const ProxiesFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.proxies,
|
||||
),
|
||||
),
|
||||
modes: hasProxies
|
||||
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
||||
: [],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.folder),
|
||||
label: "profiles",
|
||||
fragment: ProfilesFragment(),
|
||||
label: PageLabel.profiles,
|
||||
fragment: ProfilesFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.profiles,
|
||||
),
|
||||
),
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.view_timeline),
|
||||
label: "requests",
|
||||
fragment: RequestsFragment(),
|
||||
label: PageLabel.requests,
|
||||
fragment: RequestsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.requests,
|
||||
),
|
||||
),
|
||||
description: "requestsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.ballot),
|
||||
label: "connections",
|
||||
fragment: ConnectionsFragment(),
|
||||
label: PageLabel.connections,
|
||||
fragment: ConnectionsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.connections,
|
||||
),
|
||||
),
|
||||
description: "connectionsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.storage),
|
||||
label: "resources",
|
||||
label: PageLabel.resources,
|
||||
description: "resourcesDesc",
|
||||
keep: false,
|
||||
fragment: Resources(),
|
||||
fragment: Resources(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.resources,
|
||||
),
|
||||
),
|
||||
modes: [NavigationItemMode.more],
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.adb),
|
||||
label: "logs",
|
||||
fragment: const LogsFragment(),
|
||||
label: PageLabel.logs,
|
||||
fragment: const LogsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.logs,
|
||||
),
|
||||
),
|
||||
description: "logsDesc",
|
||||
modes: openLogs
|
||||
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
||||
@@ -62,8 +89,12 @@ class Navigation {
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.construction),
|
||||
label: "tools",
|
||||
fragment: ToolsFragment(),
|
||||
label: PageLabel.tools,
|
||||
fragment: ToolsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.tools,
|
||||
),
|
||||
),
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseNavigator {
|
||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
||||
if (!globalState.appController.isMobileView) {
|
||||
if (globalState.appState.viewMode != ViewMode.mobile) {
|
||||
return await Navigator.of(context).push<T>(
|
||||
CommonDesktopRoute(
|
||||
builder: (context) => child,
|
||||
@@ -68,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
|
||||
Duration get transitionDuration => const Duration(milliseconds: 500);
|
||||
|
||||
@override
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 500);
|
||||
}
|
||||
|
||||
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
|
||||
@@ -192,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,
|
||||
@@ -216,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,
|
||||
],
|
||||
),
|
||||
@@ -272,7 +273,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
|
||||
return;
|
||||
}
|
||||
|
||||
final double shadowWidth = 0.05 * configuration.size!.width;
|
||||
final double shadowWidth = 1 * configuration.size!.width;
|
||||
final double shadowHeight = configuration.size!.height;
|
||||
final double bandWidth = shadowWidth / (colors.length - 1);
|
||||
|
||||
|
||||
@@ -2,8 +2,15 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension NumExt on num {
|
||||
String fixed({digit = 2}) {
|
||||
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
|
||||
String fixed({decimals = 2}) {
|
||||
String formatted = toStringAsFixed(decimals);
|
||||
if (formatted.contains('.')) {
|
||||
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
|
||||
if (formatted.endsWith('.')) {
|
||||
formatted = formatted.substring(0, formatted.length - 1);
|
||||
}
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,19 @@ import 'dart:convert';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
import 'constant.dart';
|
||||
|
||||
class Preferences {
|
||||
static Preferences? _instance;
|
||||
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
|
||||
|
||||
Future<bool> get isInit async => await sharedPreferencesCompleter.future != null;
|
||||
Future<bool> get isInit async =>
|
||||
await sharedPreferencesCompleter.future != null;
|
||||
|
||||
Preferences._internal() {
|
||||
SharedPreferences.getInstance().then((value) => sharedPreferencesCompleter.complete(value))
|
||||
.onError((_,__)=>sharedPreferencesCompleter.complete(null));
|
||||
SharedPreferences.getInstance()
|
||||
.then((value) => sharedPreferencesCompleter.complete(value))
|
||||
.onError((_, __) => sharedPreferencesCompleter.complete(null));
|
||||
}
|
||||
|
||||
factory Preferences() {
|
||||
@@ -23,7 +24,6 @@ class Preferences {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
|
||||
Future<ClashConfig?> getClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||
@@ -32,29 +32,26 @@ class Preferences {
|
||||
return ClashConfig.fromJson(clashConfigMap);
|
||||
}
|
||||
|
||||
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
preferences?.setString(
|
||||
clashConfigKey,
|
||||
json.encode(clashConfig),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<Config?> getConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final configString = preferences?.getString(configKey);
|
||||
if (configString == null) return null;
|
||||
final configMap = json.decode(configString);
|
||||
return Config.fromJson(configMap);
|
||||
return Config.compatibleFromJson(configMap);
|
||||
}
|
||||
|
||||
Future<bool> saveConfig(Config config) async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
return await preferences?.setString(
|
||||
configKey,
|
||||
json.encode(config),
|
||||
) ?? false;
|
||||
configKey,
|
||||
json.encode(config),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
clearClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
preferences?.remove(clashConfigKey);
|
||||
}
|
||||
|
||||
clearPreferences() async {
|
||||
|
||||
31
lib/common/print.dart
Normal file
31
lib/common/print.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class CommonPrint {
|
||||
static CommonPrint? _instance;
|
||||
|
||||
CommonPrint._internal();
|
||||
|
||||
factory CommonPrint() {
|
||||
_instance ??= CommonPrint._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
log(String? text) {
|
||||
final payload = "[FlClash] $text";
|
||||
debugPrint(payload);
|
||||
if (globalState.isService) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.addLog(
|
||||
Log(
|
||||
logLevel: LogLevel.info,
|
||||
payload: payload,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final commonPrint = CommonPrint();
|
||||
@@ -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();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class Render {
|
||||
@@ -22,26 +22,26 @@ class Render {
|
||||
}
|
||||
|
||||
pause() {
|
||||
debouncer.call(
|
||||
"render_pause",
|
||||
throttler.call(
|
||||
DebounceTag.renderPause,
|
||||
_pause,
|
||||
duration: Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
|
||||
resume() {
|
||||
debouncer.cancel("render_pause");
|
||||
throttler.cancel(DebounceTag.renderPause);
|
||||
_resume();
|
||||
}
|
||||
|
||||
void _pause() {
|
||||
void _pause() async {
|
||||
if (_isPaused) return;
|
||||
_isPaused = true;
|
||||
_beginFrame = _dispatcher.onBeginFrame;
|
||||
_drawFrame = _dispatcher.onDrawFrame;
|
||||
_dispatcher.onBeginFrame = null;
|
||||
_dispatcher.onDrawFrame = null;
|
||||
debugPrint("[App] pause");
|
||||
commonPrint.log("pause");
|
||||
}
|
||||
|
||||
void _resume() {
|
||||
@@ -50,8 +50,8 @@ class Render {
|
||||
_dispatcher.onBeginFrame = _beginFrame;
|
||||
_dispatcher.onDrawFrame = _drawFrame;
|
||||
_dispatcher.scheduleFrame();
|
||||
debugPrint("[App] resume");
|
||||
commonPrint.log("resume");
|
||||
}
|
||||
}
|
||||
|
||||
final render = system.isDesktop ? Render() : null;
|
||||
final Render? render = system.isDesktop ? Render() : null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -11,33 +11,35 @@ import 'package:flutter/cupertino.dart';
|
||||
|
||||
class Request {
|
||||
late final Dio _dio;
|
||||
late final Dio _clashDio;
|
||||
String? userAgent;
|
||||
|
||||
Request() {
|
||||
_dio = Dio();
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
return handler.next(options); // 继续请求
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
headers: {
|
||||
"User-Agent": browserUa,
|
||||
},
|
||||
),
|
||||
);
|
||||
_clashDio = Dio();
|
||||
_clashDio.httpClientAdapter = IOHttpClientAdapter(createHttpClient: () {
|
||||
final client = HttpClient();
|
||||
client.findProxy = (Uri uri) {
|
||||
client.userAgent = globalState.ua;
|
||||
return FlClashHttpOverrides.handleFindProxy(uri);
|
||||
};
|
||||
return client;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Response> getFileResponseForUrl(String url) async {
|
||||
final response = await _dio
|
||||
.get(
|
||||
url,
|
||||
options: Options(
|
||||
headers: {
|
||||
"User-Agent": globalState.appController.clashConfig.globalUa
|
||||
},
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
)
|
||||
.timeout(
|
||||
httpTimeoutDuration * 6,
|
||||
);
|
||||
final response = await _clashDio.get(
|
||||
url,
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -66,36 +68,43 @@ 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;
|
||||
}
|
||||
|
||||
final List<String> _ipInfoSources = [
|
||||
"https://ipwho.is/?fields=ip&output=csv",
|
||||
"https://ipinfo.io/ip",
|
||||
"https://ifconfig.me/ip/",
|
||||
];
|
||||
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
|
||||
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||
};
|
||||
|
||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||
for (final source in _ipInfoSources) {
|
||||
for (final source in _ipInfoSources.entries) {
|
||||
try {
|
||||
final response = await _dio
|
||||
.get<String>(
|
||||
source,
|
||||
final response = await Dio()
|
||||
.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
)
|
||||
.timeout(httpTimeoutDuration);
|
||||
.timeout(
|
||||
Duration(
|
||||
seconds: 30,
|
||||
),
|
||||
);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
continue;
|
||||
}
|
||||
final ipInfo = await clashCore.getCountryCode(response.data!);
|
||||
if (ipInfo == null && source != _ipInfoSources.last) {
|
||||
if (response.data == null) {
|
||||
continue;
|
||||
}
|
||||
return ipInfo;
|
||||
return source.value(response.data!);
|
||||
} catch (e) {
|
||||
debugPrint("checkIp error ===> $e");
|
||||
commonPrint.log("checkIp error ===> $e");
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
throw "cancelled";
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
0
lib/common/state.dart
Normal file
0
lib/common/state.dart
Normal file
@@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'print.dart';
|
||||
|
||||
extension StringExtension on String {
|
||||
bool get isUrl {
|
||||
@@ -43,8 +43,17 @@ extension StringExtension on String {
|
||||
RegExp(this);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
commonPrint.log(e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StringExtensionSafe on String? {
|
||||
String getSafeValue(String defaultValue) {
|
||||
if (this == null || this!.isEmpty) {
|
||||
return defaultValue;
|
||||
}
|
||||
return this!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class System {
|
||||
} else if (Platform.isLinux) {
|
||||
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
|
||||
final output = result.stdout.trim();
|
||||
if (output.startsWith('root:') && output.contains('rwx')) {
|
||||
if (output.startsWith('root:') && output.contains('rws')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
@@ -39,10 +39,7 @@ class Tray {
|
||||
}
|
||||
|
||||
update({
|
||||
required AppState appState,
|
||||
required AppFlowingState appFlowingState,
|
||||
required Config config,
|
||||
required ClashConfig clashConfig,
|
||||
required TrayState trayState,
|
||||
bool focus = false,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
@@ -50,7 +47,7 @@ class Tray {
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
await _updateSystemTray(
|
||||
brightness: appState.brightness,
|
||||
brightness: trayState.brightness,
|
||||
force: focus,
|
||||
);
|
||||
}
|
||||
@@ -63,9 +60,7 @@ class Tray {
|
||||
);
|
||||
menuItems.add(showMenuItem);
|
||||
final startMenuItem = MenuItem.checkbox(
|
||||
label: appFlowingState.isStart
|
||||
? appLocalizations.stop
|
||||
: appLocalizations.start,
|
||||
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateStart();
|
||||
},
|
||||
@@ -80,23 +75,22 @@ class Tray {
|
||||
onClick: (_) {
|
||||
globalState.appController.changeMode(mode);
|
||||
},
|
||||
checked: mode == clashConfig.mode,
|
||||
checked: mode == trayState.mode,
|
||||
),
|
||||
);
|
||||
}
|
||||
menuItems.add(MenuItem.separator());
|
||||
if (!Platform.isWindows) {
|
||||
final groups = appState.currentGroups;
|
||||
for (final group in groups) {
|
||||
for (final group in trayState.groups) {
|
||||
List<MenuItem> subMenuItems = [];
|
||||
for (final proxy in group.all) {
|
||||
subMenuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: proxy.name,
|
||||
checked: appState.selectedMap[group.name] == proxy.name,
|
||||
checked: trayState.selectedMap[group.name] == proxy.name,
|
||||
onClick: (_) {
|
||||
final appController = globalState.appController;
|
||||
appController.config.updateCurrentSelectedMap(
|
||||
appController.updateCurrentSelectedMap(
|
||||
group.name,
|
||||
proxy.name,
|
||||
);
|
||||
@@ -117,18 +111,18 @@ class Tray {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (groups.isNotEmpty) {
|
||||
if (trayState.groups.isNotEmpty) {
|
||||
menuItems.add(MenuItem.separator());
|
||||
}
|
||||
}
|
||||
if (appFlowingState.isStart) {
|
||||
if (trayState.isStart) {
|
||||
menuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: appLocalizations.tun,
|
||||
onClick: (_) {
|
||||
globalState.appController.updateTun();
|
||||
},
|
||||
checked: clashConfig.tun.enable,
|
||||
checked: trayState.tunEnable,
|
||||
),
|
||||
);
|
||||
menuItems.add(
|
||||
@@ -137,7 +131,7 @@ class Tray {
|
||||
onClick: (_) {
|
||||
globalState.appController.updateSystemProxy();
|
||||
},
|
||||
checked: config.networkProps.systemProxy,
|
||||
checked: trayState.systemProxy,
|
||||
),
|
||||
);
|
||||
menuItems.add(MenuItem.separator());
|
||||
@@ -147,12 +141,12 @@ class Tray {
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateAutoLaunch();
|
||||
},
|
||||
checked: config.appSetting.autoLaunch,
|
||||
checked: trayState.autoLaunch,
|
||||
);
|
||||
final copyEnvVarMenuItem = MenuItem(
|
||||
label: appLocalizations.copyEnvVar,
|
||||
onClick: (_) async {
|
||||
await _copyEnv(clashConfig.mixedPort);
|
||||
await _copyEnv(trayState.port);
|
||||
},
|
||||
);
|
||||
menuItems.add(autoStartMenuItem);
|
||||
@@ -169,12 +163,25 @@ class Tray {
|
||||
await trayManager.setContextMenu(menu);
|
||||
if (Platform.isLinux) {
|
||||
await _updateSystemTray(
|
||||
brightness: appState.brightness,
|
||||
brightness: trayState.brightness,
|
||||
force: focus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateTrayTitle([Traffic? traffic]) async {
|
||||
// if (!Platform.isMacOS) {
|
||||
// return;
|
||||
// }
|
||||
// if (traffic == null) {
|
||||
// await trayManager.setTitle("");
|
||||
// } else {
|
||||
// await trayManager.setTitle(
|
||||
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> _copyEnv(int port) async {
|
||||
final url = "http://127.0.0.1:$port";
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'context.dart';
|
||||
|
||||
mixin ViewMixin<T extends StatefulWidget> on State<T> {
|
||||
List<Widget> get actions => [];
|
||||
|
||||
Widget? get floatingActionButton => null;
|
||||
|
||||
initViewState() {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.actions = actions;
|
||||
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||
commonScaffoldState?.onSearch = onSearch;
|
||||
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||
}
|
||||
|
||||
Function(String)? get onSearch => null;
|
||||
|
||||
Function(List<String>)? get onKeywordsUpdate => null;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:screen_retriever/screen_retriever.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class Window {
|
||||
init(WindowProps props, int version) async {
|
||||
init(int version) async {
|
||||
final props = globalState.config.windowProps;
|
||||
final acquire = await singleInstanceLock.acquire();
|
||||
if (!acquire) {
|
||||
exit(0);
|
||||
@@ -20,10 +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) {
|
||||
final left = props.left ?? 0;
|
||||
final top = props.top ?? 0;
|
||||
final right = left + props.width;
|
||||
@@ -60,14 +63,16 @@ class Window {
|
||||
}
|
||||
|
||||
show() async {
|
||||
render?.resume();
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
render?.resume();
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -75,9 +80,9 @@ class Window {
|
||||
}
|
||||
|
||||
hide() async {
|
||||
render?.pause();
|
||||
await windowManager.hide();
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
render?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class Windows {
|
||||
@@ -54,7 +53,7 @@ class Windows {
|
||||
calloc.free(argumentsPtr);
|
||||
calloc.free(operationPtr);
|
||||
|
||||
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
|
||||
commonPrint.log("windows runas: $command $arguments resultCode:$result");
|
||||
|
||||
if (result < 42) {
|
||||
return false;
|
||||
|
||||
@@ -8,31 +8,32 @@ import 'package:archive/archive.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/archive.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'common/common.dart';
|
||||
import 'fragments/profiles/override_profile.dart';
|
||||
import 'models/models.dart';
|
||||
|
||||
class AppController {
|
||||
final BuildContext context;
|
||||
late AppState appState;
|
||||
late AppFlowingState appFlowingState;
|
||||
late Config config;
|
||||
late ClashConfig clashConfig;
|
||||
bool lastTunEnable = false;
|
||||
int? lastProfileModified;
|
||||
|
||||
AppController(this.context) {
|
||||
appState = context.read<AppState>();
|
||||
config = context.read<Config>();
|
||||
clashConfig = context.read<ClashConfig>();
|
||||
appFlowingState = context.read<AppFlowingState>();
|
||||
}
|
||||
final BuildContext context;
|
||||
final WidgetRef _ref;
|
||||
|
||||
AppController(this.context, WidgetRef ref) : _ref = ref;
|
||||
|
||||
updateClashConfigDebounce() {
|
||||
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
|
||||
debouncer.call(DebounceTag.updateClashConfig, () async {
|
||||
final isPatch = globalState.appState.needApply ? false : true;
|
||||
await updateClashConfig(isPatch);
|
||||
});
|
||||
}
|
||||
|
||||
updateGroupsDebounce() {
|
||||
@@ -41,14 +42,16 @@ class AppController {
|
||||
|
||||
addCheckIpNumDebounce() {
|
||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||
appState.checkIpNum++;
|
||||
_ref.read(checkIpNumProvider.notifier).add();
|
||||
});
|
||||
}
|
||||
|
||||
applyProfileDebounce() {
|
||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||
applyProfile(isPrue: true);
|
||||
});
|
||||
applyProfileDebounce({
|
||||
bool silence = false,
|
||||
}) {
|
||||
debouncer.call(DebounceTag.applyProfile, (silence) {
|
||||
applyProfile(silence: silence);
|
||||
}, args: [silence]);
|
||||
}
|
||||
|
||||
savePreferencesDebounce() {
|
||||
@@ -67,11 +70,12 @@ class AppController {
|
||||
}
|
||||
|
||||
restartCore() async {
|
||||
await globalState.restartCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
);
|
||||
await clashService?.reStart();
|
||||
await _initCore();
|
||||
|
||||
if (_ref.read(runTimeProvider.notifier).isStart) {
|
||||
await globalState.handleStart();
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(bool isStart) async {
|
||||
@@ -81,13 +85,12 @@ class AppController {
|
||||
updateTraffic,
|
||||
]);
|
||||
final currentLastModified =
|
||||
await config.getCurrentProfile()?.profileLastModified;
|
||||
if (currentLastModified == null ||
|
||||
globalState.lastProfileModified == null) {
|
||||
await _ref.read(currentProfileProvider)?.profileLastModified;
|
||||
if (currentLastModified == null || lastProfileModified == null) {
|
||||
addCheckIpNumDebounce();
|
||||
return;
|
||||
}
|
||||
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
|
||||
if (currentLastModified <= (lastProfileModified ?? 0)) {
|
||||
addCheckIpNumDebounce();
|
||||
return;
|
||||
}
|
||||
@@ -95,9 +98,9 @@ class AppController {
|
||||
} else {
|
||||
await globalState.handleStop();
|
||||
await clashCore.resetTraffic();
|
||||
appFlowingState.traffics = [];
|
||||
appFlowingState.totalTraffic = Traffic();
|
||||
appFlowingState.runTime = null;
|
||||
_ref.read(trafficsProvider.notifier).clear();
|
||||
_ref.read(totalTrafficProvider.notifier).value = Traffic();
|
||||
_ref.read(runTimeProvider.notifier).value = null;
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
}
|
||||
@@ -107,107 +110,218 @@ class AppController {
|
||||
if (startTime != null) {
|
||||
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
||||
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
|
||||
_ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
|
||||
} else {
|
||||
appFlowingState.runTime = null;
|
||||
_ref.read(runTimeProvider.notifier).value = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateTraffic() {
|
||||
globalState.updateTraffic(
|
||||
config: config,
|
||||
appFlowingState: appFlowingState,
|
||||
);
|
||||
updateTraffic() async {
|
||||
final traffic = await clashCore.getTraffic();
|
||||
_ref.read(trafficsProvider.notifier).addTraffic(traffic);
|
||||
_ref.read(totalTrafficProvider.notifier).value =
|
||||
await clashCore.getTotalTraffic();
|
||||
}
|
||||
|
||||
addProfile(Profile profile) async {
|
||||
config.setProfile(profile);
|
||||
if (config.currentProfileId != null) return;
|
||||
await changeProfile(profile.id);
|
||||
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||
if (_ref.read(currentProfileIdProvider) != null) return;
|
||||
_ref.read(currentProfileIdProvider.notifier).value = profile.id;
|
||||
}
|
||||
|
||||
deleteProfile(String id) async {
|
||||
config.deleteProfileById(id);
|
||||
_ref.read(profilesProvider.notifier).deleteProfileById(id);
|
||||
clearEffect(id);
|
||||
if (config.currentProfileId == id) {
|
||||
if (config.profiles.isNotEmpty) {
|
||||
final updateId = config.profiles.first.id;
|
||||
changeProfile(updateId);
|
||||
if (globalState.config.currentProfileId == id) {
|
||||
final profiles = globalState.config.profiles;
|
||||
final currentProfileId = _ref.read(currentProfileIdProvider.notifier);
|
||||
if (profiles.isNotEmpty) {
|
||||
final updateId = profiles.first.id;
|
||||
currentProfileId.value = updateId;
|
||||
} else {
|
||||
changeProfile(null);
|
||||
currentProfileId.value = null;
|
||||
updateStatus(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateProviders() async {
|
||||
await globalState.updateProviders(appState);
|
||||
_ref.read(providersProvider.notifier).value =
|
||||
await clashCore.getExternalProviders();
|
||||
}
|
||||
|
||||
updateLocalIp() async {
|
||||
appFlowingState.localIp = null;
|
||||
_ref.read(localIpProvider.notifier).value = null;
|
||||
await Future.delayed(commonDuration);
|
||||
appFlowingState.localIp = await other.getLocalIpAddress();
|
||||
_ref.read(localIpProvider.notifier).value = await utils.getLocalIpAddress();
|
||||
}
|
||||
|
||||
Future<void> updateProfile(Profile profile) async {
|
||||
final newProfile = await profile.update();
|
||||
config.setProfile(
|
||||
newProfile.copyWith(isUpdating: false),
|
||||
);
|
||||
if (profile.id == config.currentProfile?.id) {
|
||||
applyProfileDebounce();
|
||||
_ref
|
||||
.read(profilesProvider.notifier)
|
||||
.setProfile(newProfile.copyWith(isUpdating: false));
|
||||
if (profile.id == _ref.read(currentProfileIdProvider)) {
|
||||
applyProfileDebounce(silence: true);
|
||||
}
|
||||
}
|
||||
|
||||
setProfile(Profile profile) {
|
||||
config.setProfile(profile);
|
||||
if (profile.id == config.currentProfile?.id) {
|
||||
applyProfileDebounce();
|
||||
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||
}
|
||||
|
||||
setProfileAndAutoApply(Profile profile) {
|
||||
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||
if (profile.id == _ref.read(currentProfileIdProvider)) {
|
||||
applyProfileDebounce(silence: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||
setProfiles(List<Profile> profiles) {
|
||||
_ref.read(profilesProvider.notifier).value = profiles;
|
||||
}
|
||||
|
||||
addLog(Log log) {
|
||||
_ref.read(logsProvider).add(log);
|
||||
}
|
||||
|
||||
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
|
||||
final hotKeyActions = _ref.read(hotKeyActionsProvider);
|
||||
final index =
|
||||
hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
|
||||
if (index == -1) {
|
||||
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
|
||||
..add(hotKeyAction);
|
||||
} else {
|
||||
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
|
||||
..[index] = hotKeyAction;
|
||||
}
|
||||
|
||||
_ref.read(hotKeyActionsProvider.notifier).value = index == -1
|
||||
? (List.from(hotKeyActions)..add(hotKeyAction))
|
||||
: (List.from(hotKeyActions)..[index] = hotKeyAction);
|
||||
}
|
||||
|
||||
List<Group> getCurrentGroups() {
|
||||
return _ref.read(currentGroupsStateProvider.select((state) => state.value));
|
||||
}
|
||||
|
||||
String getRealTestUrl(String? url) {
|
||||
return _ref.read(getRealTestUrlProvider(url));
|
||||
}
|
||||
|
||||
int getProxiesColumns() {
|
||||
return _ref.read(getProxiesColumnsProvider);
|
||||
}
|
||||
|
||||
addSortNum() {
|
||||
return _ref.read(sortNumProvider.notifier).add();
|
||||
}
|
||||
|
||||
getCurrentGroupName() {
|
||||
final currentGroupName = _ref.read(currentProfileProvider.select(
|
||||
(state) => state?.currentGroupName,
|
||||
));
|
||||
return currentGroupName;
|
||||
}
|
||||
|
||||
ProxyCardState getProxyCardState(proxyName) {
|
||||
return _ref.read(getProxyCardStateProvider(proxyName));
|
||||
}
|
||||
|
||||
getSelectedProxyName(groupName) {
|
||||
return _ref.read(getSelectedProxyNameProvider(groupName));
|
||||
}
|
||||
|
||||
updateCurrentGroupName(String groupName) {
|
||||
final profile = _ref.read(currentProfileProvider);
|
||||
if (profile == null || profile.currentGroupName == groupName) {
|
||||
return;
|
||||
}
|
||||
setProfile(
|
||||
profile.copyWith(currentGroupName: groupName),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig([bool? isPatch]) async {
|
||||
commonPrint.log("update clash patch: ${isPatch ?? false}");
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.updateClashConfig(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
isPatch: isPatch,
|
||||
await _updateClashConfig(
|
||||
isPatch,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future applyProfile({bool isPrue = false}) async {
|
||||
if (isPrue) {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
Future<void> _updateClashConfig([bool? isPatch]) async {
|
||||
final profile = _ref.watch(currentProfileProvider);
|
||||
await _ref.read(currentProfileProvider)?.checkAndUpdate();
|
||||
final patchConfig = _ref.read(patchClashConfigProvider);
|
||||
final appSetting = _ref.read(appSettingProvider);
|
||||
bool enableTun = patchConfig.tun.enable;
|
||||
if (enableTun != lastTunEnable &&
|
||||
lastTunEnable == false &&
|
||||
!Platform.isAndroid) {
|
||||
final code = await system.authorizeCore();
|
||||
switch (code) {
|
||||
case AuthorizeCode.none:
|
||||
break;
|
||||
case AuthorizeCode.success:
|
||||
lastTunEnable = enableTun;
|
||||
await restartCore();
|
||||
return;
|
||||
case AuthorizeCode.error:
|
||||
enableTun = false;
|
||||
}
|
||||
}
|
||||
if (appSetting.openLogs) {
|
||||
clashCore.startLog();
|
||||
} else {
|
||||
clashCore.stopLog();
|
||||
}
|
||||
final res = await clashCore.updateConfig(
|
||||
globalState.getUpdateConfigParams(isPatch),
|
||||
);
|
||||
if (isPatch == false) {
|
||||
_ref.read(needApplyProvider.notifier).value = false;
|
||||
}
|
||||
if (res.isNotEmpty) throw res;
|
||||
lastTunEnable = enableTun;
|
||||
lastProfileModified = await profile?.profileLastModified;
|
||||
}
|
||||
|
||||
Future _applyProfile() async {
|
||||
await clashCore.requestGc();
|
||||
await updateClashConfig();
|
||||
await updateGroups();
|
||||
await updateProviders();
|
||||
}
|
||||
|
||||
Future applyProfile({bool silence = false}) async {
|
||||
if (silence) {
|
||||
await _applyProfile();
|
||||
} else {
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
await _applyProfile();
|
||||
});
|
||||
}
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
changeProfile(String? value) async {
|
||||
if (value == config.currentProfileId) return;
|
||||
config.currentProfileId = value;
|
||||
handleChangeProfile() {
|
||||
_ref.read(delayDataSourceProvider.notifier).value = {};
|
||||
applyProfile();
|
||||
}
|
||||
|
||||
updateBrightness(Brightness brightness) {
|
||||
_ref.read(appBrightnessProvider.notifier).value = brightness;
|
||||
}
|
||||
|
||||
autoUpdateProfiles() async {
|
||||
for (final profile in config.profiles) {
|
||||
for (final profile in _ref.read(profilesProvider)) {
|
||||
if (!profile.autoUpdate) continue;
|
||||
final isNotNeedUpdate = profile.lastUpdateDate
|
||||
?.add(
|
||||
@@ -218,20 +332,29 @@ class AppController {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
updateProfile(profile);
|
||||
await updateProfile(profile);
|
||||
} catch (e) {
|
||||
appFlowingState.addLog(
|
||||
Log(
|
||||
logLevel: LogLevel.info,
|
||||
payload: e.toString(),
|
||||
),
|
||||
);
|
||||
_ref.read(logsProvider.notifier).addLog(
|
||||
Log(
|
||||
logLevel: LogLevel.info,
|
||||
payload: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateGroups() async {
|
||||
_ref.read(groupsProvider.notifier).value = await retry(
|
||||
task: () async {
|
||||
return await clashCore.getProxiesGroups();
|
||||
},
|
||||
retryIf: (res) => res.isEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
updateProfiles() async {
|
||||
for (final profile in config.profiles) {
|
||||
for (final profile in _ref.read(profilesProvider)) {
|
||||
if (profile.type == ProfileType.file) {
|
||||
continue;
|
||||
}
|
||||
@@ -239,34 +362,33 @@ class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateGroups() async {
|
||||
await globalState.updateGroups(appState);
|
||||
}
|
||||
|
||||
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
|
||||
appState.systemColorSchemes = systemColorSchemes;
|
||||
updateSystemColorSchemes(ColorSchemes colorSchemes) {
|
||||
_ref.read(appSchemesProvider.notifier).value = colorSchemes;
|
||||
}
|
||||
|
||||
savePreferences() async {
|
||||
debugPrint("[APP] savePreferences");
|
||||
await preferences.saveConfig(config);
|
||||
await preferences.saveClashConfig(clashConfig);
|
||||
commonPrint.log("save preferences");
|
||||
await preferences.saveConfig(globalState.config);
|
||||
}
|
||||
|
||||
changeProxy({
|
||||
required String groupName,
|
||||
required String proxyName,
|
||||
}) async {
|
||||
await globalState.changeProxy(
|
||||
config: config,
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
await clashCore.changeProxy(
|
||||
ChangeProxyParams(
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
),
|
||||
);
|
||||
if (_ref.read(appSettingProvider).closeConnections) {
|
||||
clashCore.closeConnections();
|
||||
}
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
handleBackOrExit() async {
|
||||
if (config.appSetting.minimizeOnExit) {
|
||||
if (_ref.read(appSettingProvider).minimizeOnExit) {
|
||||
if (system.isDesktop) {
|
||||
await savePreferencesDebounce();
|
||||
}
|
||||
@@ -289,7 +411,7 @@ class AppController {
|
||||
}
|
||||
|
||||
autoCheckUpdate() async {
|
||||
if (!config.appSetting.autoCheckUpdate) return;
|
||||
if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
|
||||
final res = await request.checkForUpdate();
|
||||
checkUpdateResultHandle(data: res);
|
||||
}
|
||||
@@ -298,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,
|
||||
@@ -346,7 +471,7 @@ class AppController {
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.cacheCorrupt),
|
||||
);
|
||||
if (res) {
|
||||
if (res == true) {
|
||||
final file = File(await appPath.sharedPreferencesPath);
|
||||
final isExists = await file.exists();
|
||||
if (isExists) {
|
||||
@@ -356,33 +481,43 @@ class AppController {
|
||||
await handleExit();
|
||||
}
|
||||
|
||||
Future<void> _initCore() async {
|
||||
final isInit = await clashCore.isInit;
|
||||
if (!isInit) {
|
||||
await clashCore.setState(
|
||||
globalState.getCoreState(),
|
||||
);
|
||||
await clashCore.init();
|
||||
}
|
||||
await applyProfile();
|
||||
}
|
||||
|
||||
init() async {
|
||||
await _handlePreference();
|
||||
await _handlerDisclaimer();
|
||||
await globalState.initCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
);
|
||||
await _initCore();
|
||||
await _initStatus();
|
||||
updateTray(true);
|
||||
autoLaunch?.updateStatus(
|
||||
config.appSetting.autoLaunch,
|
||||
_ref.read(appSettingProvider).autoLaunch,
|
||||
);
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
if (!_ref.read(appSettingProvider).silentLaunch) {
|
||||
window?.show();
|
||||
} else {
|
||||
window?.hide();
|
||||
}
|
||||
_ref.read(initProvider.notifier).value = true;
|
||||
}
|
||||
|
||||
_initStatus() async {
|
||||
if (Platform.isAndroid) {
|
||||
await globalState.updateStartTime();
|
||||
}
|
||||
final status =
|
||||
globalState.isStart == true ? true : config.appSetting.autoRun;
|
||||
final status = globalState.isStart == true
|
||||
? true
|
||||
: _ref.read(appSettingProvider).autoRun;
|
||||
|
||||
await updateStatus(status);
|
||||
if (!status) {
|
||||
@@ -391,35 +526,15 @@ class AppController {
|
||||
}
|
||||
|
||||
setDelay(Delay delay) {
|
||||
appState.setDelay(delay);
|
||||
_ref.read(delayDataSourceProvider.notifier).setDelay(delay);
|
||||
}
|
||||
|
||||
toPage(
|
||||
int index, {
|
||||
bool hasAnimate = false,
|
||||
}) {
|
||||
if (index > appState.currentNavigationItems.length - 1) {
|
||||
return;
|
||||
}
|
||||
appState.currentLabel = appState.currentNavigationItems[index].label;
|
||||
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
|
||||
globalState.pageController?.animateToPage(
|
||||
index,
|
||||
duration: kTabScrollDuration,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
} else {
|
||||
globalState.pageController?.jumpToPage(index);
|
||||
}
|
||||
toPage(PageLabel pageLabel) {
|
||||
_ref.read(currentPageLabelProvider.notifier).value = pageLabel;
|
||||
}
|
||||
|
||||
toProfiles() {
|
||||
final index = appState.currentNavigationItems.indexWhere(
|
||||
(element) => element.label == "profiles",
|
||||
);
|
||||
if (index != -1) {
|
||||
toPage(index);
|
||||
}
|
||||
toPage(PageLabel.profiles);
|
||||
}
|
||||
|
||||
initLink() {
|
||||
@@ -456,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: () {
|
||||
@@ -476,21 +582,24 @@ class AppController {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
disclaimerAccepted: true,
|
||||
);
|
||||
_ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(disclaimerAccepted: true),
|
||||
);
|
||||
Navigator.of(context).pop<bool>(true);
|
||||
},
|
||||
child: Text(appLocalizations.agree),
|
||||
)
|
||||
],
|
||||
child: SelectableText(
|
||||
appLocalizations.disclaimerDesc,
|
||||
),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
_handlerDisclaimer() async {
|
||||
if (config.appSetting.disclaimerAccepted) {
|
||||
if (_ref.read(appSettingProvider).disclaimerAccepted) {
|
||||
return;
|
||||
}
|
||||
final isDisclaimerAccepted = await showDisclaimer();
|
||||
@@ -549,40 +658,37 @@ class AppController {
|
||||
addProfileFormURL(url);
|
||||
}
|
||||
|
||||
updateViewWidth(double width) {
|
||||
updateViewSize(Size size) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appState.viewWidth = width;
|
||||
_ref.read(viewSizeProvider.notifier).value = size;
|
||||
});
|
||||
}
|
||||
|
||||
int? getDelay(String proxyName, [String? url]) {
|
||||
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
|
||||
return currentDelayMap?[appState.getRealProxyName(proxyName)];
|
||||
}
|
||||
|
||||
String getRealTestUrl(String? url) {
|
||||
if (url == null || url.isEmpty) {
|
||||
return config.appSetting.testUrl;
|
||||
}
|
||||
return url;
|
||||
setProvider(ExternalProvider? provider) {
|
||||
_ref.read(providersProvider.notifier).setProvider(provider);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) {
|
||||
List<Proxy> _sortOfDelay({
|
||||
required List<Proxy> proxies,
|
||||
String? testUrl,
|
||||
}) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) {
|
||||
final aDelay = getDelay(a.name, url);
|
||||
final bDelay = getDelay(b.name, url);
|
||||
final aDelay =
|
||||
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
|
||||
final bDelay =
|
||||
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
|
||||
if (aDelay == null && bDelay == null) {
|
||||
return 0;
|
||||
}
|
||||
@@ -598,69 +704,102 @@ class AppController {
|
||||
}
|
||||
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
|
||||
return switch (config.proxiesStyle.sortType) {
|
||||
return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
|
||||
ProxiesSortType.none => proxies,
|
||||
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
|
||||
ProxiesSortType.delay => _sortOfDelay(
|
||||
proxies: proxies,
|
||||
testUrl: url,
|
||||
),
|
||||
ProxiesSortType.name => _sortOfName(proxies),
|
||||
};
|
||||
}
|
||||
|
||||
String getCurrentSelectedName(String groupName) {
|
||||
final group = appState.getGroupWithName(groupName);
|
||||
return group?.getCurrentSelectedName(
|
||||
config.currentSelectedMap[groupName] ?? '') ??
|
||||
'';
|
||||
}
|
||||
|
||||
clearEffect(String profileId) async {
|
||||
final profilePath = await appPath.getProfilePath(profileId);
|
||||
final providersPath = await appPath.getProvidersPath(profileId);
|
||||
return await Isolate.run(() async {
|
||||
if (profilePath != null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool get isMobileView {
|
||||
return appState.viewMode == ViewMode.mobile;
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
);
|
||||
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(enable: !state.tun.enable),
|
||||
);
|
||||
}
|
||||
|
||||
updateSystemProxy() {
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
systemProxy: !config.networkProps.systemProxy,
|
||||
);
|
||||
_ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
systemProxy: !state.systemProxy,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
updateStart() {
|
||||
updateStatus(!appFlowingState.isStart);
|
||||
updateStatus(!_ref.read(runTimeProvider.notifier).isStart);
|
||||
}
|
||||
|
||||
updateCurrentSelectedMap(String groupName, String proxyName) {
|
||||
final currentProfile = _ref.read(currentProfileProvider);
|
||||
if (currentProfile != null &&
|
||||
currentProfile.selectedMap[groupName] != proxyName) {
|
||||
final SelectedMap selectedMap = Map.from(
|
||||
currentProfile.selectedMap,
|
||||
)..[groupName] = proxyName;
|
||||
_ref.read(profilesProvider.notifier).setProfile(
|
||||
currentProfile.copyWith(
|
||||
selectedMap: selectedMap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentUnfoldSet(Set<String> value) {
|
||||
final currentProfile = _ref.read(currentProfileProvider);
|
||||
if (currentProfile == null) {
|
||||
return;
|
||||
}
|
||||
_ref.read(profilesProvider.notifier).setProfile(
|
||||
currentProfile.copyWith(
|
||||
unfoldSet: value,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
changeMode(Mode mode) {
|
||||
clashConfig.mode = mode;
|
||||
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(mode: mode),
|
||||
);
|
||||
if (mode == Mode.global) {
|
||||
config.updateCurrentGroupName(GroupName.GLOBAL.name);
|
||||
updateCurrentGroupName(GroupName.GLOBAL.name);
|
||||
}
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
updateAutoLaunch() {
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
autoLaunch: !config.appSetting.autoLaunch,
|
||||
);
|
||||
_ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
autoLaunch: !state.autoLaunch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
updateVisible() async {
|
||||
final visible = await window?.isVisible();
|
||||
final visible = await window?.isVisible;
|
||||
if (visible != null && !visible) {
|
||||
window?.show();
|
||||
} else {
|
||||
@@ -669,24 +808,62 @@ class AppController {
|
||||
}
|
||||
|
||||
updateMode() {
|
||||
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
|
||||
if (index == -1) {
|
||||
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) {
|
||||
final index = Mode.values.indexWhere((item) => item == state.mode);
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
||||
return state.copyWith(
|
||||
mode: Mode.values[nextIndex],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
handleAddOrUpdate(WidgetRef ref, [Rule? rule]) async {
|
||||
final res = await globalState.showCommonDialog<Rule>(
|
||||
child: AddRuleDialog(
|
||||
rule: rule,
|
||||
snippet: ref.read(
|
||||
profileOverrideStateProvider.select(
|
||||
(state) => state.snippet!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (res == null) {
|
||||
return;
|
||||
}
|
||||
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
||||
clashConfig.mode = Mode.values[nextIndex];
|
||||
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 = appFlowingState.logs.map(
|
||||
(item) => item.toString(),
|
||||
);
|
||||
final logsRaw = _ref.read(logsProvider).list.map(
|
||||
(item) => item.toString(),
|
||||
);
|
||||
final data = await Isolate.run<List<int>>(() async {
|
||||
final logsRawString = logsRaw.join("\n");
|
||||
return utf8.encode(logsRawString);
|
||||
});
|
||||
return await picker.saveFile(
|
||||
other.logFile,
|
||||
utils.logFile,
|
||||
Uint8List.fromList(data),
|
||||
) !=
|
||||
null;
|
||||
@@ -695,12 +872,10 @@ class AppController {
|
||||
Future<List<int>> backupData() async {
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
final profilesPath = await appPath.profilesPath;
|
||||
final configJson = config.toJson();
|
||||
final clashConfigJson = clashConfig.toJson();
|
||||
final configJson = globalState.config.toJson();
|
||||
return Isolate.run<List<int>>(() async {
|
||||
final archive = Archive();
|
||||
archive.add("config.json", configJson);
|
||||
archive.add("clashConfig.json", clashConfigJson);
|
||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||
final zipEncoder = ZipEncoder();
|
||||
return zipEncoder.encode(archive) ?? [];
|
||||
@@ -709,11 +884,7 @@ class AppController {
|
||||
|
||||
updateTray([bool focus = false]) async {
|
||||
tray.update(
|
||||
appState: appState,
|
||||
appFlowingState: appFlowingState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
focus: focus,
|
||||
trayState: _ref.read(trayStateProvider),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -732,32 +903,64 @@ class AppController {
|
||||
archive.files.where((item) => !item.name.endsWith(".json"));
|
||||
final configIndex =
|
||||
configs.indexWhere((config) => config.name == "config.json");
|
||||
final clashConfigIndex =
|
||||
configs.indexWhere((config) => config.name == "clashConfig.json");
|
||||
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
|
||||
if (configIndex == -1) throw "invalid backup file";
|
||||
final configFile = configs[configIndex];
|
||||
final clashConfigFile = configs[clashConfigIndex];
|
||||
final tempConfig = Config.fromJson(
|
||||
var tempConfig = Config.compatibleFromJson(
|
||||
json.decode(
|
||||
utf8.decode(configFile.content),
|
||||
),
|
||||
);
|
||||
final tempClashConfig = ClashConfig.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(clashConfigFile.content),
|
||||
),
|
||||
);
|
||||
for (final profile in profiles) {
|
||||
final filePath = join(homeDirPath, profile.name);
|
||||
final file = File(filePath);
|
||||
await file.create(recursive: true);
|
||||
await file.writeAsBytes(profile.content);
|
||||
}
|
||||
if (recoveryOption == RecoveryOption.onlyProfiles) {
|
||||
config.update(tempConfig, RecoveryOption.onlyProfiles);
|
||||
} else {
|
||||
config.update(tempConfig, RecoveryOption.all);
|
||||
clashConfig.update(tempClashConfig);
|
||||
final clashConfigIndex =
|
||||
configs.indexWhere((config) => config.name == "clashConfig.json");
|
||||
if (clashConfigIndex != -1) {
|
||||
final clashConfigFile = configs[clashConfigIndex];
|
||||
tempConfig = tempConfig.copyWith(
|
||||
patchClashConfig: ClashConfig.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(
|
||||
clashConfigFile.content,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
_recovery(
|
||||
tempConfig,
|
||||
recoveryOption,
|
||||
);
|
||||
}
|
||||
|
||||
_recovery(Config config, RecoveryOption recoveryOption) {
|
||||
final profiles = config.profiles;
|
||||
for (final profile in profiles) {
|
||||
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||
}
|
||||
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
|
||||
if (onlyProfiles) {
|
||||
final currentProfile = _ref.read(currentProfileProvider);
|
||||
if (currentProfile != null) {
|
||||
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ref.read(patchClashConfigProvider.notifier).value =
|
||||
config.patchClashConfig;
|
||||
_ref.read(appSettingProvider.notifier).value = config.appSetting;
|
||||
_ref.read(currentProfileIdProvider.notifier).value =
|
||||
config.currentProfileId;
|
||||
_ref.read(appDAVSettingProvider.notifier).value = config.dav;
|
||||
_ref.read(themeSettingProvider.notifier).value = config.themeProps;
|
||||
_ref.read(windowSettingProvider.notifier).value = config.windowProps;
|
||||
_ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
|
||||
_ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle;
|
||||
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
|
||||
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
|
||||
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,24 @@ const desktopPlatforms = [
|
||||
SupportPlatform.Windows,
|
||||
];
|
||||
|
||||
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
||||
enum GroupType {
|
||||
Selector,
|
||||
URLTest,
|
||||
Fallback,
|
||||
LoadBalance,
|
||||
Relay;
|
||||
|
||||
static GroupType parseProfileType(String type) {
|
||||
return switch (type) {
|
||||
"url-test" => URLTest,
|
||||
"select" => Selector,
|
||||
"fallback" => Fallback,
|
||||
"load-balance" => LoadBalance,
|
||||
"relay" => Relay,
|
||||
String() => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
||||
|
||||
@@ -45,7 +62,7 @@ extension GroupTypeExtension on GroupType {
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool get isURLTestOrFallback {
|
||||
bool get isComputedSelected {
|
||||
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
||||
}
|
||||
|
||||
@@ -138,6 +155,13 @@ enum DnsMode {
|
||||
hosts
|
||||
}
|
||||
|
||||
enum ExternalControllerStatus {
|
||||
@JsonValue("")
|
||||
close,
|
||||
@JsonValue("127.0.0.1:9090")
|
||||
open
|
||||
}
|
||||
|
||||
enum KeyboardModifier {
|
||||
alt([
|
||||
PhysicalKeyboardKey.altLeft,
|
||||
@@ -195,14 +219,13 @@ enum ProxiesIconStyle {
|
||||
}
|
||||
|
||||
enum FontFamily {
|
||||
system(),
|
||||
miSans("MiSans"),
|
||||
twEmoji("Twemoji"),
|
||||
jetBrainsMono("JetBrainsMono"),
|
||||
icon("Icons");
|
||||
|
||||
final String? value;
|
||||
final String value;
|
||||
|
||||
const FontFamily([this.value]);
|
||||
const FontFamily(this.value);
|
||||
}
|
||||
|
||||
enum RouteMode {
|
||||
@@ -238,6 +261,7 @@ enum ActionMethod {
|
||||
stopListener,
|
||||
getCountryCode,
|
||||
getMemory,
|
||||
getProfile,
|
||||
|
||||
///Android,
|
||||
setFdMap,
|
||||
@@ -270,7 +294,11 @@ enum DebounceTag {
|
||||
handleWill,
|
||||
updateDelay,
|
||||
vpnTip,
|
||||
autoLaunch
|
||||
autoLaunch,
|
||||
renderPause,
|
||||
updatePageIndex,
|
||||
pageChange,
|
||||
proxiesTabChange,
|
||||
}
|
||||
|
||||
enum DashboardWidget {
|
||||
@@ -341,3 +369,81 @@ enum DashboardWidget {
|
||||
return dashboardWidgets[index];
|
||||
}
|
||||
}
|
||||
|
||||
enum GeodataLoader {
|
||||
standard,
|
||||
memconservative,
|
||||
}
|
||||
|
||||
enum PageLabel {
|
||||
dashboard,
|
||||
proxies,
|
||||
profiles,
|
||||
tools,
|
||||
logs,
|
||||
requests,
|
||||
resources,
|
||||
connections,
|
||||
}
|
||||
|
||||
enum RuleAction {
|
||||
DOMAIN("DOMAIN"),
|
||||
DOMAIN_SUFFIX("DOMAIN-SUFFIX"),
|
||||
DOMAIN_KEYWORD("DOMAIN-KEYWORD"),
|
||||
DOMAIN_REGEX("DOMAIN-REGEX"),
|
||||
GEOSITE("GEOSITE"),
|
||||
IP_CIDR("IP-CIDR"),
|
||||
IP_CIDR6("IP-CIDR6"),
|
||||
IP_SUFFIX("IP-SUFFIX"),
|
||||
IP_ASN("IP-ASN"),
|
||||
GEOIP("GEOIP"),
|
||||
SRC_GEOIP("SRC-GEOIP"),
|
||||
SRC_IP_ASN("SRC-IP-ASN"),
|
||||
SRC_IP_CIDR("SRC-IP-CIDR"),
|
||||
SRC_IP_SUFFIX("SRC-IP-SUFFIX"),
|
||||
DST_PORT("DST-PORT"),
|
||||
SRC_PORT("SRC-PORT"),
|
||||
IN_PORT("IN-PORT"),
|
||||
IN_TYPE("IN-TYPE"),
|
||||
IN_USER("IN-USER"),
|
||||
IN_NAME("IN-NAME"),
|
||||
PROCESS_PATH("PROCESS-PATH"),
|
||||
PROCESS_PATH_REGEX("PROCESS-PATH-REGEX"),
|
||||
PROCESS_NAME("PROCESS-NAME"),
|
||||
PROCESS_NAME_REGEX("PROCESS-NAME-REGEX"),
|
||||
UID("UID"),
|
||||
NETWORK("NETWORK"),
|
||||
DSCP("DSCP"),
|
||||
RULE_SET("RULE-SET"),
|
||||
AND("AND"),
|
||||
OR("OR"),
|
||||
NOT("NOT"),
|
||||
SUB_RULE("SUB-RULE"),
|
||||
MATCH("MATCH");
|
||||
|
||||
final String value;
|
||||
|
||||
const RuleAction(this.value);
|
||||
}
|
||||
|
||||
extension RuleActionExt on RuleAction {
|
||||
bool get hasParams => [
|
||||
RuleAction.GEOIP,
|
||||
RuleAction.IP_ASN,
|
||||
RuleAction.SRC_IP_ASN,
|
||||
RuleAction.IP_CIDR,
|
||||
RuleAction.IP_CIDR6,
|
||||
RuleAction.IP_SUFFIX,
|
||||
RuleAction.RULE_SET,
|
||||
].contains(this);
|
||||
}
|
||||
|
||||
enum OverrideRuleType {
|
||||
override,
|
||||
added,
|
||||
}
|
||||
|
||||
enum RuleTarget {
|
||||
DIRECT,
|
||||
REJECT,
|
||||
}
|
||||
|
||||
@@ -4,20 +4,21 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class AccessFragment extends StatefulWidget {
|
||||
class AccessFragment extends ConsumerStatefulWidget {
|
||||
const AccessFragment({super.key});
|
||||
|
||||
@override
|
||||
State<AccessFragment> createState() => _AccessFragmentState();
|
||||
ConsumerState<AccessFragment> createState() => _AccessFragmentState();
|
||||
}
|
||||
|
||||
class _AccessFragmentState extends State<AccessFragment> {
|
||||
class _AccessFragmentState extends ConsumerState<AccessFragment> {
|
||||
List<String> acceptList = [];
|
||||
List<String> rejectList = [];
|
||||
late ScrollController _controller;
|
||||
@@ -28,10 +29,11 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
_updateInitList();
|
||||
_controller = ScrollController();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
final appState = globalState.appState;
|
||||
if (appState.packages.isEmpty) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||
appState.packages = await app?.getPackages() ?? [];
|
||||
ref.read(packagesProvider.notifier).value =
|
||||
await app?.getPackages() ?? [];
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -44,9 +46,8 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
}
|
||||
|
||||
_updateInitList() {
|
||||
final accessControl = globalState.appController.config.accessControl;
|
||||
acceptList = accessControl.acceptList;
|
||||
rejectList = accessControl.rejectList;
|
||||
acceptList = globalState.config.vpnProps.accessControl.acceptList;
|
||||
rejectList = globalState.config.vpnProps.accessControl.rejectList;
|
||||
}
|
||||
|
||||
Widget _buildSearchButton() {
|
||||
@@ -59,9 +60,13 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
acceptList: acceptList,
|
||||
rejectList: rejectList,
|
||||
),
|
||||
).then((_) => setState(() {
|
||||
).then(
|
||||
(_) => setState(
|
||||
() {
|
||||
_updateInitList();
|
||||
}));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
);
|
||||
@@ -77,28 +82,29 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
return IconButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
final isAccept =
|
||||
config.accessControl.mode == AccessControlMode.acceptSelected;
|
||||
if (isSelectedAll) {
|
||||
config.accessControl = switch (isAccept) {
|
||||
true => config.accessControl.copyWith(
|
||||
acceptList: [],
|
||||
),
|
||||
false => config.accessControl.copyWith(
|
||||
rejectList: [],
|
||||
),
|
||||
};
|
||||
} else {
|
||||
config.accessControl = switch (isAccept) {
|
||||
true => config.accessControl.copyWith(
|
||||
acceptList: allValueList,
|
||||
),
|
||||
false => config.accessControl.copyWith(
|
||||
rejectList: allValueList,
|
||||
),
|
||||
};
|
||||
}
|
||||
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||
final isAccept =
|
||||
state.accessControl.mode == AccessControlMode.acceptSelected;
|
||||
if (isSelectedAll) {
|
||||
return switch (isAccept) {
|
||||
true => state.copyWith.accessControl(
|
||||
acceptList: [],
|
||||
),
|
||||
false => state.copyWith.accessControl(
|
||||
rejectList: [],
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return switch (isAccept) {
|
||||
true => state.copyWith.accessControl(
|
||||
acceptList: allValueList,
|
||||
),
|
||||
false => state.copyWith.accessControl(
|
||||
rejectList: allValueList,
|
||||
),
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: isSelectedAll
|
||||
? const Icon(Icons.deselect)
|
||||
@@ -106,223 +112,247 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
title: appLocalizations.proxiesSetting,
|
||||
context: context,
|
||||
body: AccessControlWidget(
|
||||
context: context,
|
||||
_intelligentSelected() async {
|
||||
final appState = globalState.appState;
|
||||
final config = globalState.config;
|
||||
final accessControl = config.vpnProps.accessControl;
|
||||
final packageNames = appState.packages
|
||||
.where(
|
||||
(item) =>
|
||||
accessControl.isFilterSystemApp ? item.isSystem == false : true,
|
||||
)
|
||||
.map((item) => item.packageName);
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
final selectedPackageNames =
|
||||
(await commonScaffoldState?.loadingRun<List<String>>(
|
||||
() async {
|
||||
return await app?.getChinaPackageNames() ?? [];
|
||||
},
|
||||
))
|
||||
?.toSet() ??
|
||||
{};
|
||||
final acceptList = packageNames
|
||||
.where((item) => !selectedPackageNames.contains(item))
|
||||
.toList();
|
||||
final rejectList = packageNames
|
||||
.where((item) => selectedPackageNames.contains(item))
|
||||
.toList();
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
acceptList: acceptList,
|
||||
rejectList: rejectList,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingButton() {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
final res = await showSheet<int>(
|
||||
context: context,
|
||||
props: SheetProps(
|
||||
isScrollControlled: true,
|
||||
),
|
||||
builder: (_, type) {
|
||||
return AdaptiveSheetScaffold(
|
||||
type: type,
|
||||
body: AccessControlPanel(),
|
||||
title: appLocalizations.proxiesSetting,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (res == 1) {
|
||||
_intelligentSelected();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.tune),
|
||||
);
|
||||
}
|
||||
|
||||
_handleSelected(List<String> valueList, Package package, bool? value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||
return switch (
|
||||
state.accessControl.mode == AccessControlMode.acceptSelected) {
|
||||
true => state.copyWith.accessControl(
|
||||
acceptList: valueList,
|
||||
),
|
||||
false => state.copyWith.accessControl(
|
||||
rejectList: valueList,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.isAccessControl,
|
||||
builder: (_, isAccessControl, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: ListItem.switchItem(
|
||||
title: Text(appLocalizations.appAccessControl),
|
||||
delegate: SwitchDelegate(
|
||||
value: isAccessControl,
|
||||
onChanged: (isAccessControl) {
|
||||
final config = context.read<Config>();
|
||||
config.isAccessControl = isAccessControl;
|
||||
},
|
||||
),
|
||||
),
|
||||
final state = ref.watch(packageListSelectorStateProvider);
|
||||
final accessControl = state.accessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages = state.getList(
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? acceptList
|
||||
: rejectList,
|
||||
);
|
||||
final currentList = accessControl.currentList;
|
||||
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
final describe = accessControlMode == AccessControlMode.acceptSelected
|
||||
? appLocalizations.accessControlAllowDesc
|
||||
: appLocalizations.accessControlNotAllowDesc;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: ListItem.switchItem(
|
||||
title: Text(appLocalizations.appAccessControl),
|
||||
delegate: SwitchDelegate(
|
||||
value: accessControl.enable,
|
||||
onChanged: (enable) {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
enable: enable,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Divider(
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: child!,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Selector<AppState, List<Package>>(
|
||||
selector: (_, appState) => appState.packages,
|
||||
builder: (_, packages, ___) {
|
||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
||||
selector: (_, appState, config) => PackageListSelectorState(
|
||||
accessControl: config.accessControl,
|
||||
isAccessControl: config.isAccessControl,
|
||||
packages: appState.packages,
|
||||
),
|
||||
builder: (context, state, __) {
|
||||
final accessControl = state.accessControl;
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages = state.getList(
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? acceptList
|
||||
: rejectList,
|
||||
);
|
||||
final currentList = accessControl.currentList;
|
||||
final packageNameList =
|
||||
packages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
final describe =
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? appLocalizations.accessControlAllowDesc
|
||||
: appLocalizations.accessControlNotAllowDesc;
|
||||
return DisabledMask(
|
||||
status: !isAccessControl,
|
||||
child: Column(
|
||||
children: [
|
||||
ActivateBox(
|
||||
active: isAccessControl,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: 16,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
appLocalizations.selected,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Flexible(
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
"${valueList.length}",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(describe),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Divider(
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: DisabledMask(
|
||||
status: !accessControl.enable,
|
||||
child: Column(
|
||||
children: [
|
||||
ActivateBox(
|
||||
active: accessControl.enable,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: 16,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: _buildSearchButton(),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSelectedAllButton(
|
||||
isSelectedAll: valueList.length ==
|
||||
packageNameList.length,
|
||||
allValueList: packageNameList,
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
appLocalizations.selected,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Flexible(
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
"${valueList.length}",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSettingButton(),
|
||||
),
|
||||
child: Text(describe),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: _buildSearchButton(),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSelectedAllButton(
|
||||
isSelectedAll:
|
||||
valueList.length == packageNameList.length,
|
||||
allValueList: packageNameList,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSettingButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: packages.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonScrollBar(
|
||||
controller: _controller,
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
itemCount: packages.length,
|
||||
itemExtent: 72,
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
value:
|
||||
valueList.contains(package.packageName),
|
||||
isActive: isAccessControl,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
final config =
|
||||
globalState.appController.config;
|
||||
if (accessControlMode ==
|
||||
AccessControlMode.acceptSelected) {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
acceptList: valueList,
|
||||
);
|
||||
} else {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
rejectList: valueList,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: packages.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonScrollBar(
|
||||
controller: _controller,
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
itemCount: packages.length,
|
||||
itemExtent: 72,
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
value: valueList.contains(package.packageName),
|
||||
isActive: accessControl.enable,
|
||||
onChanged: (value) {
|
||||
_handleSelected(valueList, package, value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -343,45 +373,47 @@ class PackageListItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActivateBox(
|
||||
active: isActive,
|
||||
child: ListItem.checkbox(
|
||||
leading: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: FutureBuilder<ImageProvider?>(
|
||||
future: app?.getPackageIcon(package.packageName),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Image(
|
||||
image: snapshot.data!,
|
||||
gaplessPlayback: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
},
|
||||
return FadeScaleEnterBox(
|
||||
child: ActivateBox(
|
||||
active: isActive,
|
||||
child: ListItem.checkbox(
|
||||
leading: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: FutureBuilder<ImageProvider?>(
|
||||
future: app?.getPackageIcon(package.packageName),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Image(
|
||||
image: snapshot.data!,
|
||||
gaplessPlayback: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
package.label,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
title: Text(
|
||||
package.label,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
package.packageName,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
subtitle: Text(
|
||||
package.packageName,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
delegate: CheckboxDelegate(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
delegate: CheckboxDelegate(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -426,15 +458,31 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
);
|
||||
}
|
||||
|
||||
_handleSelected(
|
||||
WidgetRef ref, List<String> valueList, Package package, bool? value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||
return switch (
|
||||
state.accessControl.mode == AccessControlMode.acceptSelected) {
|
||||
true => state.copyWith.accessControl(
|
||||
acceptList: valueList,
|
||||
),
|
||||
false => state.copyWith.accessControl(
|
||||
rejectList: valueList,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Widget _packageList() {
|
||||
final lowQuery = query.toLowerCase();
|
||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
||||
selector: (_, appState, config) => PackageListSelectorState(
|
||||
packages: appState.packages,
|
||||
accessControl: config.accessControl,
|
||||
isAccessControl: config.isAccessControl,
|
||||
),
|
||||
builder: (context, state, __) {
|
||||
return Consumer(
|
||||
builder: (context, ref, __) {
|
||||
final state = ref.watch(packageListSelectorStateProvider);
|
||||
final accessControl = state.accessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages = state.getList(
|
||||
@@ -449,7 +497,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
package.packageName.contains(lowQuery),
|
||||
)
|
||||
.toList();
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final isAccessControl = state.accessControl.enable;
|
||||
final currentList = accessControl.currentList;
|
||||
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
@@ -465,21 +513,12 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
value: valueList.contains(package.packageName),
|
||||
isActive: isAccessControl,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
if (accessControlMode == AccessControlMode.acceptSelected) {
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
acceptList: valueList,
|
||||
);
|
||||
} else {
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
rejectList: valueList,
|
||||
);
|
||||
}
|
||||
_handleSelected(
|
||||
ref,
|
||||
valueList,
|
||||
package,
|
||||
value,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -500,14 +539,16 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
class AccessControlWidget extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
|
||||
const AccessControlWidget({
|
||||
class AccessControlPanel extends ConsumerStatefulWidget {
|
||||
const AccessControlPanel({
|
||||
super.key,
|
||||
required this.context,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState createState() => _AccessControlPanelState();
|
||||
}
|
||||
|
||||
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
|
||||
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
||||
return switch (mode) {
|
||||
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
||||
@@ -552,9 +593,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, AccessControlMode>(
|
||||
selector: (_, config) => config.accessControl.mode,
|
||||
builder: (_, accessControlMode, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final accessControlMode = ref.watch(
|
||||
vpnSettingProvider.select((state) => state.accessControl.mode),
|
||||
);
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -566,10 +609,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
),
|
||||
isSelected: accessControlMode == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
mode: item,
|
||||
);
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
mode: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -588,9 +632,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, AccessSortType>(
|
||||
selector: (_, config) => config.accessControl.sort,
|
||||
builder: (_, accessSortType, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final accessSortType = ref.watch(
|
||||
vpnSettingProvider.select((state) => state.accessControl.sort),
|
||||
);
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -602,10 +648,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
),
|
||||
isSelected: accessSortType == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
sort: item,
|
||||
);
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
sort: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -624,9 +671,12 @@ class AccessControlWidget extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.accessControl.isFilterSystemApp,
|
||||
builder: (_, isFilterSystemApp, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final isFilterSystemApp = ref.watch(
|
||||
vpnSettingProvider
|
||||
.select((state) => state.accessControl.isFilterSystemApp),
|
||||
);
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -635,10 +685,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
_getTextWithIsFilterSystemApp(item),
|
||||
isSelected: isFilterSystemApp == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
isFilterSystemApp: item,
|
||||
);
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
isFilterSystemApp: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -650,63 +701,35 @@ class AccessControlWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
_intelligentSelected() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final config = globalState.appController.config;
|
||||
final accessControl = config.accessControl;
|
||||
final packageNames = appState.packages
|
||||
.where(
|
||||
(item) =>
|
||||
accessControl.isFilterSystemApp ? item.isSystem == false : true,
|
||||
)
|
||||
.map((item) => item.packageName);
|
||||
Navigator.of(context).pop();
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
final selectedPackageNames =
|
||||
(await commonScaffoldState?.loadingRun<List<String>>(
|
||||
() async {
|
||||
return await app?.getChinaPackageNames() ?? [];
|
||||
},
|
||||
))
|
||||
?.toSet() ??
|
||||
{};
|
||||
final acceptList = packageNames
|
||||
.where((item) => !selectedPackageNames.contains(item))
|
||||
.toList();
|
||||
final rejectList = packageNames
|
||||
.where((item) => selectedPackageNames.contains(item))
|
||||
.toList();
|
||||
config.accessControl = accessControl.copyWith(
|
||||
acceptList: acceptList,
|
||||
rejectList: rejectList,
|
||||
);
|
||||
}
|
||||
|
||||
_copyToClipboard() async {
|
||||
await globalState.safeRun(() {
|
||||
final data = globalState.appController.config.accessControl.toJson();
|
||||
final data = globalState.config.vpnProps.accessControl.toJson();
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: json.encode(data),
|
||||
),
|
||||
);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
_pasteToClipboard() async {
|
||||
await globalState.safeRun(() async {
|
||||
final config = globalState.appController.config;
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text == null) return;
|
||||
config.accessControl = AccessControl.fromJson(
|
||||
json.decode(text),
|
||||
);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
await globalState.safeRun(
|
||||
() async {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text == null) return;
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
accessControl: AccessControl.fromJson(
|
||||
json.decode(text),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@@ -725,7 +748,9 @@ class AccessControlWidget extends StatelessWidget {
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.auto_awesome),
|
||||
label: appLocalizations.intelligentSelected,
|
||||
onPressed: _intelligentSelected,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(1);
|
||||
},
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.paste),
|
||||
@@ -746,17 +771,19 @@ class AccessControlWidget extends StatelessWidget {
|
||||
|
||||
@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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,61 +1,258 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CloseConnectionsSwitch extends StatelessWidget {
|
||||
const CloseConnectionsSwitch({super.key});
|
||||
class CloseConnectionsItem extends ConsumerWidget {
|
||||
const CloseConnectionsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.closeConnections,
|
||||
builder: (_, closeConnections, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoCloseConnections),
|
||||
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: closeConnections,
|
||||
onChanged: (value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
closeConnections: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final closeConnections = ref.watch(
|
||||
appSettingProvider.select((state) => state.closeConnections),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoCloseConnections),
|
||||
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: closeConnections,
|
||||
onChanged: (value) async {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
closeConnections: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UsageSwitch extends StatelessWidget {
|
||||
const UsageSwitch({super.key});
|
||||
class UsageItem extends ConsumerWidget {
|
||||
const UsageItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.onlyStatisticsProxy,
|
||||
builder: (_, onlyProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: onlyProxy,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
onlyStatisticsProxy: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final onlyStatisticsProxy = ref.watch(
|
||||
appSettingProvider.select((state) => state.onlyStatisticsProxy),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: onlyStatisticsProxy,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
onlyStatisticsProxy: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MinimizeItem extends ConsumerWidget {
|
||||
const MinimizeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final minimizeOnExit = ref.watch(
|
||||
appSettingProvider.select((state) => state.minimizeOnExit),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.minimizeOnExit),
|
||||
subtitle: Text(appLocalizations.minimizeOnExitDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: minimizeOnExit,
|
||||
onChanged: (bool value) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
minimizeOnExit: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoLaunchItem extends ConsumerWidget {
|
||||
const AutoLaunchItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final autoLaunch = ref.watch(
|
||||
appSettingProvider.select((state) => state.autoLaunch),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoLaunch),
|
||||
subtitle: Text(appLocalizations.autoLaunchDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: autoLaunch,
|
||||
onChanged: (bool value) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
autoLaunch: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SilentLaunchItem extends ConsumerWidget {
|
||||
const SilentLaunchItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final silentLaunch = ref.watch(
|
||||
appSettingProvider.select((state) => state.silentLaunch),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.silentLaunch),
|
||||
subtitle: Text(appLocalizations.silentLaunchDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: silentLaunch,
|
||||
onChanged: (bool value) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
silentLaunch: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoRunItem extends ConsumerWidget {
|
||||
const AutoRunItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final autoRun = ref.watch(
|
||||
appSettingProvider.select((state) => state.autoRun),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoRun),
|
||||
subtitle: Text(appLocalizations.autoRunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: autoRun,
|
||||
onChanged: (bool value) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
autoRun: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenItem extends ConsumerWidget {
|
||||
const HiddenItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hidden = ref.watch(
|
||||
appSettingProvider.select((state) => state.hidden),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.exclude),
|
||||
subtitle: Text(appLocalizations.excludeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: hidden,
|
||||
onChanged: (value) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
hidden: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimateTabItem extends ConsumerWidget {
|
||||
const AnimateTabItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isAnimateToPage = ref.watch(
|
||||
appSettingProvider.select((state) => state.isAnimateToPage),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.tabAnimation),
|
||||
subtitle: Text(appLocalizations.tabAnimationDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isAnimateToPage,
|
||||
onChanged: (value) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
isAnimateToPage: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OpenLogsItem extends ConsumerWidget {
|
||||
const OpenLogsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final openLogs = ref.watch(
|
||||
appSettingProvider.select((state) => state.openLogs),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.logcat),
|
||||
subtitle: Text(appLocalizations.logcatDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: openLogs,
|
||||
onChanged: (bool value) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
openLogs: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoCheckUpdateItem extends ConsumerWidget {
|
||||
const AutoCheckUpdateItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final autoCheckUpdate = ref.watch(
|
||||
appSettingProvider.select((state) => state.autoCheckUpdate),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoCheckUpdate),
|
||||
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: autoCheckUpdate,
|
||||
onChanged: (bool value) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
autoCheckUpdate: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,156 +268,20 @@ class ApplicationSettingFragment extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> items = [
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.minimizeOnExit,
|
||||
builder: (_, isMinimizeOnExit, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.minimizeOnExit),
|
||||
subtitle: Text(appLocalizations.minimizeOnExitDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isMinimizeOnExit,
|
||||
onChanged: (bool value) {
|
||||
final config = context.read<Config>();
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
minimizeOnExit: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (system.isDesktop)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.autoLaunch,
|
||||
builder: (_, autoLaunch, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoLaunch),
|
||||
subtitle: Text(appLocalizations.autoLaunchDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: autoLaunch,
|
||||
onChanged: (bool value) {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
autoLaunch: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (system.isDesktop)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.silentLaunch,
|
||||
builder: (_, silentLaunch, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.silentLaunch),
|
||||
subtitle: Text(appLocalizations.silentLaunchDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: silentLaunch,
|
||||
onChanged: (bool value) {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
silentLaunch: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.autoRun,
|
||||
builder: (_, autoRun, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoRun),
|
||||
subtitle: Text(appLocalizations.autoRunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: autoRun,
|
||||
onChanged: (bool value) {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
autoRun: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.hidden,
|
||||
builder: (_, isExclude, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.exclude),
|
||||
subtitle: Text(appLocalizations.excludeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isExclude,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
hidden: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.isAnimateToPage,
|
||||
builder: (_, isAnimateToPage, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.tabAnimation),
|
||||
subtitle: Text(appLocalizations.tabAnimationDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isAnimateToPage,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
isAnimateToPage: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.openLogs,
|
||||
builder: (_, openLogs, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.logcat),
|
||||
subtitle: Text(appLocalizations.logcatDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: openLogs,
|
||||
onChanged: (bool value) {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
openLogs: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const CloseConnectionsSwitch(),
|
||||
const UsageSwitch(),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.autoCheckUpdate,
|
||||
builder: (_, autoCheckUpdate, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoCheckUpdate),
|
||||
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: autoCheckUpdate,
|
||||
onChanged: (bool value) {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
autoCheckUpdate: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MinimizeItem(),
|
||||
if (system.isDesktop) ...[
|
||||
AutoLaunchItem(),
|
||||
SilentLaunchItem(),
|
||||
],
|
||||
AutoRunItem(),
|
||||
if (Platform.isAndroid) ...[
|
||||
HiddenItem(),
|
||||
],
|
||||
AnimateTabItem(),
|
||||
OpenLogsItem(),
|
||||
CloseConnectionsItem(),
|
||||
UsageItem(),
|
||||
AutoCheckUpdateItem(),
|
||||
];
|
||||
return ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
|
||||
@@ -4,14 +4,16 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/dav_client.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/dialog.dart';
|
||||
import 'package:fl_clash/widgets/fade_box.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
import 'package:fl_clash/widgets/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class BackupAndRecovery extends StatelessWidget {
|
||||
class BackupAndRecovery extends ConsumerWidget {
|
||||
const BackupAndRecovery({super.key});
|
||||
|
||||
_showAddWebDAV(DAV? dav) async {
|
||||
@@ -73,7 +75,7 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
() async {
|
||||
final backupData = await globalState.appController.backupData();
|
||||
final value = await picker.saveFile(
|
||||
other.getBackupFileName(),
|
||||
utils.getBackupFileName(),
|
||||
Uint8List.fromList(backupData),
|
||||
);
|
||||
if (value == null) return false;
|
||||
@@ -121,139 +123,140 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
_recoveryOnLocal(context, recoveryOption);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, DAV?>(
|
||||
selector: (_, config) => config.dav,
|
||||
builder: (_, dav, __) {
|
||||
final client = dav != null ? DAVClient(dav) : null;
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.remote),
|
||||
if (dav == null)
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: Text(appLocalizations.noInfo),
|
||||
subtitle: Text(appLocalizations.pleaseBindWebDAV),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.bind,
|
||||
),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: TooltipText(
|
||||
text: Text(
|
||||
dav.user,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(appLocalizations.connectivity),
|
||||
FutureBuilder<bool>(
|
||||
future: client!.pingCompleter.future,
|
||||
builder: (_, snapshot) {
|
||||
return Center(
|
||||
child: FadeBox(
|
||||
child: snapshot.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: snapshot.data == true
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.edit,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
ListItem.input(
|
||||
title: Text(appLocalizations.file),
|
||||
subtitle: Text(dav.fileName),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.file,
|
||||
value: dav.fileName,
|
||||
resetValue: defaultDavFileName,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.config.dav =
|
||||
globalState.appController.config.dav?.copyWith(
|
||||
fileName: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.remoteBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.remoteRecoveryDesc),
|
||||
),
|
||||
],
|
||||
ListHeader(title: appLocalizations.local),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.localBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.localRecoveryDesc),
|
||||
),
|
||||
],
|
||||
_handleChange(String? value, WidgetRef ref) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(appDAVSettingProvider.notifier).updateState(
|
||||
(state) => state?.copyWith(
|
||||
fileName: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final dav = ref.watch(appDAVSettingProvider);
|
||||
final client = dav != null ? DAVClient(dav) : null;
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.remote),
|
||||
if (dav == null)
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: Text(appLocalizations.noInfo),
|
||||
subtitle: Text(appLocalizations.pleaseBindWebDAV),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.bind,
|
||||
),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: TooltipText(
|
||||
text: Text(
|
||||
dav.user,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(appLocalizations.connectivity),
|
||||
FutureBuilder<bool>(
|
||||
future: client!.pingCompleter.future,
|
||||
builder: (_, snapshot) {
|
||||
return Center(
|
||||
child: FadeThroughBox(
|
||||
child: snapshot.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: snapshot.data == true
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.edit,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
ListItem.input(
|
||||
title: Text(appLocalizations.file),
|
||||
subtitle: Text(dav.fileName),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.file,
|
||||
value: dav.fileName,
|
||||
resetValue: defaultDavFileName,
|
||||
onChanged: (value) {
|
||||
_handleChange(value, ref);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.remoteBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.remoteRecoveryDesc),
|
||||
),
|
||||
],
|
||||
ListHeader(title: appLocalizations.local),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.localBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.localRecoveryDesc),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -273,45 +276,42 @@ 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),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WebDAVFormDialog extends StatefulWidget {
|
||||
class WebDAVFormDialog extends ConsumerStatefulWidget {
|
||||
final DAV? dav;
|
||||
|
||||
const WebDAVFormDialog({super.key, this.dav});
|
||||
|
||||
@override
|
||||
State<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
|
||||
ConsumerState<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
|
||||
}
|
||||
|
||||
class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
|
||||
late TextEditingController uriController;
|
||||
late TextEditingController userController;
|
||||
late TextEditingController passwordController;
|
||||
@@ -328,7 +328,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
|
||||
_submit() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
globalState.appController.config.dav = DAV(
|
||||
ref.read(appDAVSettingProvider.notifier).value = DAV(
|
||||
uri: uriController.text,
|
||||
user: userController.text,
|
||||
password: passwordController.text,
|
||||
@@ -337,7 +337,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
}
|
||||
|
||||
_delete() {
|
||||
globalState.appController.config.dav = null;
|
||||
ref.read(appDAVSettingProvider.notifier).value = null;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@@ -349,78 +349,8 @@ class _WebDAVFormDialogState extends State<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(
|
||||
@@ -432,6 +362,73 @@ class _WebDAVFormDialogState extends State<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,208 +1,196 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class OverrideItem extends StatelessWidget {
|
||||
class OverrideItem extends ConsumerWidget {
|
||||
const OverrideItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.overrideDns,
|
||||
builder: (_, override, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.overrideDns),
|
||||
subtitle: Text(appLocalizations.overrideDnsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: override,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.overrideDns = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final override = ref.watch(overrideDnsProvider);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.overrideDns),
|
||||
subtitle: Text(appLocalizations.overrideDnsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: override,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(overrideDnsProvider.notifier).value = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusItem extends StatelessWidget {
|
||||
class StatusItem extends ConsumerWidget {
|
||||
const StatusItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.enable,
|
||||
builder: (_, enable, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.status),
|
||||
subtitle: Text(appLocalizations.statusDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final enable =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.dns.enable));
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.status),
|
||||
subtitle: Text(appLocalizations.statusDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(enable: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PreferH3Item extends StatelessWidget {
|
||||
class ListenItem extends ConsumerWidget {
|
||||
const ListenItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final listen =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.dns.listen));
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.listen),
|
||||
subtitle: Text(listen),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.listen,
|
||||
value: listen,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(listen: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PreferH3Item extends ConsumerWidget {
|
||||
const PreferH3Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.preferH3,
|
||||
builder: (_, preferH3, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("PreferH3"),
|
||||
subtitle: Text(appLocalizations.preferH3Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: preferH3,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
preferH3: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final preferH3 = ref
|
||||
.watch(patchClashConfigProvider.select((state) => state.dns.preferH3));
|
||||
return ListItem.switchItem(
|
||||
title: const Text("PreferH3"),
|
||||
subtitle: Text(appLocalizations.preferH3Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: preferH3,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(preferH3: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IPv6Item extends StatelessWidget {
|
||||
class IPv6Item extends ConsumerWidget {
|
||||
const IPv6Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
ipv6: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ipv6 = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.ipv6),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(ipv6: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RespectRulesItem extends StatelessWidget {
|
||||
class RespectRulesItem extends ConsumerWidget {
|
||||
const RespectRulesItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.respectRules,
|
||||
builder: (_, respectRules, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.respectRules),
|
||||
subtitle: Text(appLocalizations.respectRulesDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: respectRules,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
respectRules: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final respectRules = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.respectRules),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.respectRules),
|
||||
subtitle: Text(appLocalizations.respectRulesDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: respectRules,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(respectRules: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DnsModeItem extends StatelessWidget {
|
||||
class DnsModeItem extends ConsumerWidget {
|
||||
const DnsModeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, DnsMode>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
|
||||
builder: (_, enhancedMode, __) {
|
||||
return ListItem<DnsMode>.options(
|
||||
title: Text(appLocalizations.dnsMode),
|
||||
subtitle: Text(enhancedMode.name),
|
||||
delegate: OptionsDelegate(
|
||||
title: appLocalizations.dnsMode,
|
||||
options: DnsMode.values,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(enhancedMode: value);
|
||||
},
|
||||
textBuilder: (dnsMode) => dnsMode.name,
|
||||
value: enhancedMode,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final enhancedMode = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.enhancedMode),
|
||||
);
|
||||
return ListItem<DnsMode>.options(
|
||||
title: Text(appLocalizations.dnsMode),
|
||||
subtitle: Text(enhancedMode.name),
|
||||
delegate: OptionsDelegate(
|
||||
title: appLocalizations.dnsMode,
|
||||
options: DnsMode.values,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(enhancedMode: value));
|
||||
},
|
||||
textBuilder: (dnsMode) => dnsMode.name,
|
||||
value: enhancedMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeIpRangeItem extends StatelessWidget {
|
||||
class FakeIpRangeItem extends ConsumerWidget {
|
||||
const FakeIpRangeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
|
||||
builder: (_, fakeIpRange, __) {
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.fakeipRange),
|
||||
subtitle: Text(fakeIpRange),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.fakeipRange,
|
||||
value: fakeIpRange,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.dns = clashConfig.dns.copyWith(
|
||||
fakeIpRange: value,
|
||||
);
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.fakeipRange,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final fakeIpRange = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.fakeIpRange),
|
||||
);
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.fakeipRange),
|
||||
subtitle: Text(fakeIpRange),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.fakeipRange,
|
||||
value: fakeIpRange,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(fakeIpRange: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -215,27 +203,28 @@ class FakeIpFilterItem extends StatelessWidget {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.fakeipFilter),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.fakeipFilter,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, fakeIpFilter, __) {
|
||||
return ListPage(
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final fakeIpFilter = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fakeIpFilter),
|
||||
);
|
||||
return ListInputPage(
|
||||
title: appLocalizations.fakeipFilter,
|
||||
items: fakeIpFilter,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fakeIpFilter: List.from(items),
|
||||
);
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
fakeIpFilter: List.from(items),
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -250,27 +239,26 @@ class DefaultNameserverItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.defaultNameserver),
|
||||
subtitle: Text(appLocalizations.defaultNameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.defaultNameserver,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, defaultNameserver, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
defaultNameserver: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final defaultNameserver = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.defaultNameserver),
|
||||
);
|
||||
return ListInputPage(
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns(
|
||||
defaultNameserver: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -286,79 +274,71 @@ class NameserverItem extends StatelessWidget {
|
||||
subtitle: Text(appLocalizations.nameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.nameserver,
|
||||
isBlur: false,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.nameserver,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, nameserver, __) {
|
||||
return ListPage(
|
||||
title: "域名服务器",
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
nameserver: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
blur: false,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final nameserver = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.nameserver),
|
||||
);
|
||||
return ListInputPage(
|
||||
title: appLocalizations.nameserver,
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns(
|
||||
nameserver: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UseHostsItem extends StatelessWidget {
|
||||
class UseHostsItem extends ConsumerWidget {
|
||||
const UseHostsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.useHosts,
|
||||
builder: (_, useHosts, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useHosts,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
useHosts: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final useHosts = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.useHosts),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useHosts,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(useHosts: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UseSystemHostsItem extends StatelessWidget {
|
||||
class UseSystemHostsItem extends ConsumerWidget {
|
||||
const UseSystemHostsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
|
||||
builder: (_, useSystemHosts, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useSystemHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useSystemHosts,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
useSystemHosts: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final useSystemHosts = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.useSystemHosts),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useSystemHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useSystemHosts,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
useSystemHosts: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -372,29 +352,27 @@ class NameserverPolicyItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.nameserverPolicy),
|
||||
subtitle: Text(appLocalizations.nameserverPolicyDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
widget: Selector<ClashConfig, Map<String, String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const MapEquality<String, String>().equals(prev, next),
|
||||
builder: (_, nameserverPolicy, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
nameserverPolicy: Map.fromEntries(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final nameserverPolicy = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.nameserverPolicy),
|
||||
);
|
||||
return MapInputPage(
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
map: nameserverPolicy,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (value) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns(
|
||||
nameserverPolicy: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -409,27 +387,28 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.proxyNameserver),
|
||||
subtitle: Text(appLocalizations.proxyNameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.proxyNameserver,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, proxyServerNameserver, __) {
|
||||
return ListPage(
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final proxyServerNameserver = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.proxyServerNameserver),
|
||||
);
|
||||
return ListInputPage(
|
||||
title: appLocalizations.proxyNameserver,
|
||||
items: proxyServerNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
proxyServerNameserver: List.from(items),
|
||||
);
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns(
|
||||
proxyServerNameserver: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -444,95 +423,81 @@ class FallbackItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.fallback),
|
||||
subtitle: Text(appLocalizations.fallbackDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.fallback,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallback,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, fallback, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallback: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final fallback = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.fallback),
|
||||
);
|
||||
return ListInputPage(
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns(
|
||||
fallback: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeoipItem extends StatelessWidget {
|
||||
class GeoipItem extends ConsumerWidget {
|
||||
const GeoipItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
|
||||
builder: (_, geoip, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("Geoip"),
|
||||
delegate: SwitchDelegate(
|
||||
value: geoip,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final geoip = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.geoip),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: const Text("Geoip"),
|
||||
delegate: SwitchDelegate(
|
||||
value: geoip,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||
geoip: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeoipCodeItem extends StatelessWidget {
|
||||
class GeoipCodeItem extends ConsumerWidget {
|
||||
const GeoipCodeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
|
||||
builder: (_, geoipCode, __) {
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.geoipCode),
|
||||
subtitle: Text(geoipCode),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.geoipCode,
|
||||
value: geoipCode,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
geoipCode: value,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.geoipCode,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final geoipCode = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.geoipCode),
|
||||
);
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.geoipCode),
|
||||
subtitle: Text(geoipCode),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.geoipCode,
|
||||
value: geoipCode,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns.fallbackFilter(
|
||||
geoipCode: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -545,29 +510,26 @@ class GeositeItem extends StatelessWidget {
|
||||
return ListItem.open(
|
||||
title: const Text("Geosite"),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: "Geosite",
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, geosite, __) {
|
||||
return ListPage(
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
geosite: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final geosite = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.geosite),
|
||||
);
|
||||
return ListInputPage(
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns.fallbackFilter(
|
||||
geosite: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -581,29 +543,26 @@ class IpcidrItem extends StatelessWidget {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.ipcidr),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.ipcidr,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, ipcidr, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
ipcidr: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
widget: Consumer(builder: (_, ref, ___) {
|
||||
final ipcidr = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.ipcidr),
|
||||
);
|
||||
return ListInputPage(
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||
ipcidr: List.from(items),
|
||||
));
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -617,29 +576,26 @@ class DomainItem extends StatelessWidget {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.domain),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: appLocalizations.domain,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, domain, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
domain: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
widget: Consumer(builder: (_, ref, __) {
|
||||
final domain = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.domain),
|
||||
);
|
||||
return ListInputPage(
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.dns.fallbackFilter(
|
||||
domain: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -655,6 +611,7 @@ class DnsOptions extends StatelessWidget {
|
||||
title: appLocalizations.options,
|
||||
items: [
|
||||
const StatusItem(),
|
||||
const ListenItem(),
|
||||
const UseHostsItem(),
|
||||
const UseSystemHostsItem(),
|
||||
const IPv6Item(),
|
||||
@@ -700,37 +657,11 @@ const dnsItems = <Widget>[
|
||||
FallbackFilterOptions(),
|
||||
];
|
||||
|
||||
class DnsListView extends StatelessWidget {
|
||||
class DnsListView extends ConsumerWidget {
|
||||
const DnsListView({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.clashConfig.dns = defaultDns;
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
Icons.replay,
|
||||
),
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
Widget build(BuildContext context, ref) {
|
||||
return generateListView(
|
||||
dnsItems,
|
||||
);
|
||||
|
||||
@@ -1,198 +1,190 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class LogLevelItem extends StatelessWidget {
|
||||
class LogLevelItem extends ConsumerWidget {
|
||||
const LogLevelItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, LogLevel>(
|
||||
selector: (_, clashConfig) => clashConfig.logLevel,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<LogLevel>.options(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(appLocalizations.logLevel),
|
||||
subtitle: Text(value.name),
|
||||
delegate: OptionsDelegate<LogLevel>(
|
||||
title: appLocalizations.logLevel,
|
||||
options: LogLevel.values,
|
||||
onChanged: (LogLevel? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.logLevel = value;
|
||||
},
|
||||
textBuilder: (logLevel) => logLevel.name,
|
||||
value: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final logLevel =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.logLevel));
|
||||
return ListItem<LogLevel>.options(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(appLocalizations.logLevel),
|
||||
subtitle: Text(logLevel.name),
|
||||
delegate: OptionsDelegate<LogLevel>(
|
||||
title: appLocalizations.logLevel,
|
||||
options: LogLevel.values,
|
||||
onChanged: (LogLevel? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
logLevel: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
textBuilder: (logLevel) => logLevel.name,
|
||||
value: logLevel,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UaItem extends StatelessWidget {
|
||||
class UaItem extends ConsumerWidget {
|
||||
const UaItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String?>(
|
||||
selector: (_, clashConfig) => clashConfig.globalRealUa,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<String?>.options(
|
||||
leading: const Icon(Icons.computer_outlined),
|
||||
title: const Text("UA"),
|
||||
subtitle: Text(value ?? appLocalizations.defaultText),
|
||||
delegate: OptionsDelegate<String?>(
|
||||
title: "UA",
|
||||
options: [
|
||||
null,
|
||||
"clash-verge/v1.6.6",
|
||||
"ClashforWindows/0.19.23",
|
||||
],
|
||||
value: value,
|
||||
onChanged: (ua) {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.globalRealUa = ua;
|
||||
},
|
||||
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final globalUa =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.globalUa));
|
||||
return ListItem<String?>.options(
|
||||
leading: const Icon(Icons.computer_outlined),
|
||||
title: const Text("UA"),
|
||||
subtitle: Text(globalUa ?? appLocalizations.defaultText),
|
||||
delegate: OptionsDelegate<String?>(
|
||||
title: "UA",
|
||||
options: [
|
||||
null,
|
||||
"clash-verge/v1.6.6",
|
||||
"ClashforWindows/0.19.23",
|
||||
],
|
||||
value: globalUa,
|
||||
onChanged: (value) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
globalUa: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeepAliveIntervalItem extends StatelessWidget {
|
||||
class KeepAliveIntervalItem extends ConsumerWidget {
|
||||
const KeepAliveIntervalItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, int>(
|
||||
selector: (_, config) => config.keepAliveInterval,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timer_outlined),
|
||||
title: Text(appLocalizations.keepAliveIntervalDesc),
|
||||
subtitle: Text("$value ${appLocalizations.seconds}"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
suffixText: appLocalizations.seconds,
|
||||
resetValue: "$defaultKeepAliveInterval",
|
||||
value: "$value",
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final intValue = int.parse(value);
|
||||
if (intValue <= 0) {
|
||||
throw "Invalid keepAliveInterval";
|
||||
}
|
||||
globalState.appController.clashConfig.keepAliveInterval =
|
||||
intValue;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
Widget build(BuildContext context, ref) {
|
||||
final keepAliveInterval = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.keepAliveInterval));
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timer_outlined),
|
||||
title: Text(appLocalizations.keepAliveIntervalDesc),
|
||||
subtitle: Text("$keepAliveInterval ${appLocalizations.seconds}"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
suffixText: appLocalizations.seconds,
|
||||
resetValue: "$defaultKeepAliveInterval",
|
||||
value: "$keepAliveInterval",
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.safeRun(
|
||||
() {
|
||||
final intValue = int.parse(value);
|
||||
if (intValue <= 0) {
|
||||
throw "Invalid keepAliveInterval";
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
keepAliveInterval: intValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
silence: false,
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TestUrlItem extends StatelessWidget {
|
||||
class TestUrlItem extends ConsumerWidget {
|
||||
const TestUrlItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, String>(
|
||||
selector: (_, config) => config.appSetting.testUrl,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timeline),
|
||||
title: Text(appLocalizations.testUrl),
|
||||
subtitle: Text(value),
|
||||
delegate: InputDelegate(
|
||||
resetValue: defaultTestUrl,
|
||||
title: appLocalizations.testUrl,
|
||||
value: value,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
if (!value.isUrl) {
|
||||
throw "Invalid url";
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
testUrl: value,
|
||||
);
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.testUrl,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
Widget build(BuildContext context, ref) {
|
||||
final testUrl =
|
||||
ref.watch(appSettingProvider.select((state) => state.testUrl));
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timeline),
|
||||
title: Text(appLocalizations.testUrl),
|
||||
subtitle: Text(testUrl),
|
||||
delegate: InputDelegate(
|
||||
resetValue: defaultTestUrl,
|
||||
title: appLocalizations.testUrl,
|
||||
value: testUrl,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.safeRun(
|
||||
() {
|
||||
if (!value.isUrl) {
|
||||
throw "Invalid url";
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
testUrl: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
silence: false,
|
||||
title: appLocalizations.testUrl,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MixedPortItem extends StatelessWidget {
|
||||
class MixedPortItem extends ConsumerWidget {
|
||||
const MixedPortItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, int>(
|
||||
selector: (_, clashConfig) => clashConfig.mixedPort,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.adjust_outlined),
|
||||
title: Text(appLocalizations.proxyPort),
|
||||
subtitle: Text("$value"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.proxyPort,
|
||||
value: "$value",
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final mixedPort = int.parse(value);
|
||||
if (mixedPort < 1024 || mixedPort > 49151) {
|
||||
throw "Invalid port";
|
||||
}
|
||||
globalState.appController.clashConfig.mixedPort = mixedPort;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.proxyPort,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
Widget build(BuildContext context, ref) {
|
||||
final mixedPort =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.mixedPort));
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.adjust_outlined),
|
||||
title: Text(appLocalizations.proxyPort),
|
||||
subtitle: Text("$mixedPort"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.proxyPort,
|
||||
value: "$mixedPort",
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.safeRun(
|
||||
() {
|
||||
final mixedPort = int.parse(value);
|
||||
if (mixedPort < 1024 || mixedPort > 49151) {
|
||||
throw "Invalid port";
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
mixedPort: mixedPort,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetValue: "$defaultMixedPort",
|
||||
),
|
||||
);
|
||||
},
|
||||
silence: false,
|
||||
title: appLocalizations.proxyPort,
|
||||
);
|
||||
},
|
||||
resetValue: "$defaultMixedPort",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -207,216 +199,218 @@ class HostsItem extends StatelessWidget {
|
||||
title: const Text("Hosts"),
|
||||
subtitle: Text(appLocalizations.hostsDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
blur: false,
|
||||
title: "Hosts",
|
||||
widget: Selector<ClashConfig, HostsMap>(
|
||||
selector: (_, clashConfig) => clashConfig.hosts,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const MapEquality<String, String>().equals(prev, next),
|
||||
builder: (_, hosts, ___) {
|
||||
final entries = hosts.entries;
|
||||
return ListPage(
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final hosts = ref
|
||||
.watch(patchClashConfigProvider.select((state) => state.hosts));
|
||||
return MapInputPage(
|
||||
title: "Hosts",
|
||||
items: entries,
|
||||
map: hosts,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items){
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.hosts = Map.fromEntries(items);
|
||||
onChange: (value) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
hosts: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Ipv6Item extends StatelessWidget {
|
||||
class Ipv6Item extends ConsumerWidget {
|
||||
const Ipv6Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.water_outlined),
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.ipv6 = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ipv6 =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.ipv6));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.water_outlined),
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
ipv6: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AllowLanItem extends StatelessWidget {
|
||||
class AllowLanItem extends ConsumerWidget {
|
||||
const AllowLanItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.allowLan,
|
||||
builder: (_, allowLan, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.device_hub),
|
||||
title: Text(appLocalizations.allowLan),
|
||||
subtitle: Text(appLocalizations.allowLanDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowLan,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = context.read<ClashConfig>();
|
||||
clashConfig.allowLan = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final allowLan =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.allowLan));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.device_hub),
|
||||
title: Text(appLocalizations.allowLan),
|
||||
subtitle: Text(appLocalizations.allowLanDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowLan,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
allowLan: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedDelayItem extends StatelessWidget {
|
||||
class UnifiedDelayItem extends ConsumerWidget {
|
||||
const UnifiedDelayItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.unifiedDelay,
|
||||
builder: (_, unifiedDelay, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.compress_outlined),
|
||||
title: Text(appLocalizations.unifiedDelay),
|
||||
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: unifiedDelay,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.unifiedDelay = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final unifiedDelay = ref
|
||||
.watch(patchClashConfigProvider.select((state) => state.unifiedDelay));
|
||||
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.compress_outlined),
|
||||
title: Text(appLocalizations.unifiedDelay),
|
||||
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: unifiedDelay,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
unifiedDelay: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FindProcessItem extends StatelessWidget {
|
||||
class FindProcessItem extends ConsumerWidget {
|
||||
const FindProcessItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.findProcessMode == FindProcessMode.always,
|
||||
builder: (_, findProcess, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.polymer_outlined),
|
||||
title: Text(appLocalizations.findProcessMode),
|
||||
subtitle: Text(appLocalizations.findProcessModeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: findProcess,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.findProcessMode =
|
||||
value ? FindProcessMode.always : FindProcessMode.off;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final findProcess = ref.watch(patchClashConfigProvider
|
||||
.select((state) => state.findProcessMode == FindProcessMode.always));
|
||||
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.polymer_outlined),
|
||||
title: Text(appLocalizations.findProcessMode),
|
||||
subtitle: Text(appLocalizations.findProcessModeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: findProcess,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
findProcessMode:
|
||||
value ? FindProcessMode.always : FindProcessMode.off,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TcpConcurrentItem extends StatelessWidget {
|
||||
class TcpConcurrentItem extends ConsumerWidget {
|
||||
const TcpConcurrentItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
|
||||
builder: (_, tcpConcurrent, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.double_arrow_outlined),
|
||||
title: Text(appLocalizations.tcpConcurrent),
|
||||
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: tcpConcurrent,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.tcpConcurrent = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final tcpConcurrent = ref
|
||||
.watch(patchClashConfigProvider.select((state) => state.tcpConcurrent));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.double_arrow_outlined),
|
||||
title: Text(appLocalizations.tcpConcurrent),
|
||||
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: tcpConcurrent,
|
||||
onChanged: (value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
tcpConcurrent: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeodataLoaderItem extends StatelessWidget {
|
||||
class GeodataLoaderItem extends ConsumerWidget {
|
||||
const GeodataLoaderItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.geodataLoader == geodataLoaderMemconservative,
|
||||
builder: (_, memconservative, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: Text(appLocalizations.geodataLoader),
|
||||
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: memconservative,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.geodataLoader =
|
||||
value ? geodataLoaderMemconservative : geodataLoaderStandard;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final isMemconservative = ref.watch(patchClashConfigProvider.select(
|
||||
(state) => state.geodataLoader == GeodataLoader.memconservative));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: Text(appLocalizations.geodataLoader),
|
||||
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isMemconservative,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
geodataLoader: value
|
||||
? GeodataLoader.memconservative
|
||||
: GeodataLoader.standard,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalControllerItem extends StatelessWidget {
|
||||
class ExternalControllerItem extends ConsumerWidget {
|
||||
const ExternalControllerItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
|
||||
builder: (_, hasExternalController, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.api_outlined),
|
||||
title: Text(appLocalizations.externalController),
|
||||
subtitle: Text(appLocalizations.externalControllerDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: hasExternalController,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.externalController =
|
||||
value ? defaultExternalController : '';
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final hasExternalController = ref.watch(patchClashConfigProvider.select(
|
||||
(state) => state.externalController == ExternalControllerStatus.open));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.api_outlined),
|
||||
title: Text(appLocalizations.externalController),
|
||||
subtitle: Text(appLocalizations.externalControllerDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: hasExternalController,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
externalController: value
|
||||
? ExternalControllerStatus.open
|
||||
: ExternalControllerStatus.close,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final generalItems = const [
|
||||
final generalItems = <Widget>[
|
||||
LogLevelItem(),
|
||||
UaItem(),
|
||||
KeepAliveIntervalItem(),
|
||||
if (system.isDesktop) KeepAliveIntervalItem(),
|
||||
TestUrlItem(),
|
||||
MixedPortItem(),
|
||||
HostsItem(),
|
||||
|
||||
@@ -3,200 +3,185 @@ import 'dart:io';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VPNItem extends StatelessWidget {
|
||||
class VPNItem extends ConsumerWidget {
|
||||
const VPNItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.enable,
|
||||
builder: (_, enable, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("VPN"),
|
||||
subtitle: Text(appLocalizations.vpnEnableDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.vpnProps = config.vpnProps.copyWith(
|
||||
enable: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final enable =
|
||||
ref.watch(vpnSettingProvider.select((state) => state.enable));
|
||||
return ListItem.switchItem(
|
||||
title: const Text("VPN"),
|
||||
subtitle: Text(appLocalizations.vpnEnableDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (value) async {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
enable: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TUNItem extends StatelessWidget {
|
||||
class TUNItem extends ConsumerWidget {
|
||||
const TUNItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||
builder: (_, enable, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.tun),
|
||||
subtitle: Text(appLocalizations.tunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final enable =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.tun.enable));
|
||||
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.tun),
|
||||
subtitle: Text(appLocalizations.tunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(
|
||||
enable: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AllowBypassItem extends StatelessWidget {
|
||||
class AllowBypassItem extends ConsumerWidget {
|
||||
const AllowBypassItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.allowBypass,
|
||||
builder: (_, allowBypass, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.allowBypass),
|
||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowBypass,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
allowBypass: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final allowBypass =
|
||||
ref.watch(vpnSettingProvider.select((state) => state.allowBypass));
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.allowBypass),
|
||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowBypass,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
allowBypass: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VpnSystemProxyItem extends StatelessWidget {
|
||||
class VpnSystemProxyItem extends ConsumerWidget {
|
||||
const VpnSystemProxyItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
systemProxy: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final systemProxy =
|
||||
ref.watch(vpnSettingProvider.select((state) => state.systemProxy));
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
systemProxy: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemProxyItem extends StatelessWidget {
|
||||
class SystemProxyItem extends ConsumerWidget {
|
||||
const SystemProxyItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.networkProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final networkProps = config.networkProps;
|
||||
config.networkProps = networkProps.copyWith(
|
||||
systemProxy: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final systemProxy =
|
||||
ref.watch(networkSettingProvider.select((state) => state.systemProxy));
|
||||
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
systemProxy: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Ipv6Item extends StatelessWidget {
|
||||
class Ipv6Item extends ConsumerWidget {
|
||||
const Ipv6Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
ipv6: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ipv6 = ref.watch(vpnSettingProvider.select((state) => state.ipv6));
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
ipv6: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TunStackItem extends StatelessWidget {
|
||||
class TunStackItem extends ConsumerWidget {
|
||||
const TunStackItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, TunStack>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.stack,
|
||||
builder: (_, stack, __) {
|
||||
return ListItem.options(
|
||||
title: Text(appLocalizations.stackMode),
|
||||
subtitle: Text(stack.name),
|
||||
delegate: OptionsDelegate<TunStack>(
|
||||
value: stack,
|
||||
options: TunStack.values,
|
||||
textBuilder: (value) => value.name,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
stack: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final stack =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.tun.stack));
|
||||
|
||||
return ListItem.options(
|
||||
title: Text(appLocalizations.stackMode),
|
||||
subtitle: Text(stack.name),
|
||||
delegate: OptionsDelegate<TunStack>(
|
||||
value: stack,
|
||||
options: TunStack.values,
|
||||
textBuilder: (value) => value.name,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(
|
||||
stack: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
title: appLocalizations.stackMode,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
title: appLocalizations.stackMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -204,7 +189,7 @@ class TunStackItem extends StatelessWidget {
|
||||
class BypassDomainItem extends StatelessWidget {
|
||||
const BypassDomainItem({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
_initActions(BuildContext context, WidgetRef ref) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
@@ -218,10 +203,11 @@ class BypassDomainItem extends StatelessWidget {
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
@@ -238,103 +224,98 @@ class BypassDomainItem extends StatelessWidget {
|
||||
title: Text(appLocalizations.bypassDomain),
|
||||
subtitle: Text(appLocalizations.bypassDomainDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
blur: false,
|
||||
title: appLocalizations.bypassDomain,
|
||||
widget: Selector<Config, List<String>>(
|
||||
selector: (_, config) => config.networkProps.bypassDomain,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (context, bypassDomain, __) {
|
||||
_initActions(context);
|
||||
return ListPage(
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
_initActions(context, ref);
|
||||
final bypassDomain = ref.watch(
|
||||
networkSettingProvider.select((state) => state.bypassDomain));
|
||||
return ListInputPage(
|
||||
title: appLocalizations.bypassDomain,
|
||||
items: bypassDomain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: List.from(items),
|
||||
);
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
bypassDomain: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RouteModeItem extends StatelessWidget {
|
||||
class RouteModeItem extends ConsumerWidget {
|
||||
const RouteModeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, RouteMode>(
|
||||
selector: (_, clashConfig) => clashConfig.routeMode,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<RouteMode>.options(
|
||||
title: Text(appLocalizations.routeMode),
|
||||
subtitle: Text(Intl.message("routeMode_${value.name}")),
|
||||
delegate: OptionsDelegate<RouteMode>(
|
||||
title: appLocalizations.routeMode,
|
||||
options: RouteMode.values,
|
||||
onChanged: (RouteMode? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.routeMode = value;
|
||||
},
|
||||
textBuilder: (routeMode) => Intl.message(
|
||||
"routeMode_${routeMode.name}",
|
||||
),
|
||||
value: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final routeMode =
|
||||
ref.watch(networkSettingProvider.select((state) => state.routeMode));
|
||||
return ListItem<RouteMode>.options(
|
||||
title: Text(appLocalizations.routeMode),
|
||||
subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
|
||||
delegate: OptionsDelegate<RouteMode>(
|
||||
title: appLocalizations.routeMode,
|
||||
options: RouteMode.values,
|
||||
onChanged: (RouteMode? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
routeMode: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
textBuilder: (routeMode) => Intl.message(
|
||||
"routeMode_${routeMode.name}",
|
||||
),
|
||||
value: routeMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RouteAddressItem extends StatelessWidget {
|
||||
class RouteAddressItem extends ConsumerWidget {
|
||||
const RouteAddressItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config,
|
||||
builder: (_, value, child) {
|
||||
if (value) {
|
||||
return child!;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
child: ListItem.open(
|
||||
title: Text(appLocalizations.routeAddress),
|
||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
title: appLocalizations.routeAddress,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!stringListEquality.equals(prev, next),
|
||||
builder: (context, routeAddress, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.routeAddress,
|
||||
items: routeAddress,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.includeRouteAddress =
|
||||
Set<String>.from(items).toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final bypassPrivate = ref.watch(networkSettingProvider
|
||||
.select((state) => state.routeMode == RouteMode.bypassPrivate));
|
||||
if (bypassPrivate) {
|
||||
return Container();
|
||||
}
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.routeAddress),
|
||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
||||
delegate: OpenDelegate(
|
||||
blur: false,
|
||||
maxWidth: 360,
|
||||
title: appLocalizations.routeAddress,
|
||||
widget: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final routeAddress = ref.watch(patchClashConfigProvider
|
||||
.select((state) => state.tun.routeAddress));
|
||||
return ListInputPage(
|
||||
title: appLocalizations.routeAddress,
|
||||
items: routeAddress,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(
|
||||
routeAddress: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -347,7 +328,8 @@ final networkItems = [
|
||||
...generateSection(
|
||||
title: "VPN",
|
||||
items: [
|
||||
const SystemProxyItem(),
|
||||
const VpnSystemProxyItem(),
|
||||
const BypassDomainItem(),
|
||||
const AllowBypassItem(),
|
||||
const Ipv6Item(),
|
||||
],
|
||||
@@ -371,10 +353,10 @@ final networkItems = [
|
||||
),
|
||||
];
|
||||
|
||||
class NetworkListView extends StatelessWidget {
|
||||
class NetworkListView extends ConsumerWidget {
|
||||
const NetworkListView({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
_initActions(BuildContext context, WidgetRef ref) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
@@ -388,9 +370,14 @@ class NetworkListView extends StatelessWidget {
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.config.vpnProps = defaultVpnProps;
|
||||
appController.clashConfig.tun = defaultTun;
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => defaultVpnProps,
|
||||
);
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
tun: defaultTun,
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
@@ -402,8 +389,8 @@ class NetworkListView extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
Widget build(BuildContext context, ref) {
|
||||
_initActions(context, ref);
|
||||
return generateListView(
|
||||
networkItems,
|
||||
);
|
||||
|
||||
@@ -4,21 +4,23 @@ import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'item.dart';
|
||||
|
||||
class ConnectionsFragment extends StatefulWidget {
|
||||
class ConnectionsFragment extends ConsumerStatefulWidget {
|
||||
const ConnectionsFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
|
||||
ConsumerState<ConnectionsFragment> createState() =>
|
||||
_ConnectionsFragmentState();
|
||||
}
|
||||
|
||||
class _ConnectionsFragmentState extends State<ConnectionsFragment>
|
||||
with ViewMixin {
|
||||
class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
|
||||
with PageMixin {
|
||||
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
|
||||
const ConnectionsState(),
|
||||
);
|
||||
@@ -71,15 +73,20 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateConnections();
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
initViewState();
|
||||
ref.listenManual(
|
||||
isCurrentPageProvider(
|
||||
PageLabel.connections,
|
||||
handler: (pageLabel, viewMode) =>
|
||||
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||
),
|
||||
(prev, next) {
|
||||
if (prev != next && next == true) {
|
||||
initPageState();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
_updateConnections();
|
||||
}
|
||||
|
||||
_handleBlockConnection(String id) async {
|
||||
@@ -100,56 +107,44 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, bool?>(
|
||||
selector: (_, appState) =>
|
||||
appState.currentLabel == 'connections' ||
|
||||
appState.viewMode == ViewMode.mobile &&
|
||||
appState.currentLabel == "tools",
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent == null || isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _connectionsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullConnectionsDesc,
|
||||
);
|
||||
}
|
||||
return CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
final connection = connections[index];
|
||||
return ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(connection.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
),
|
||||
return ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _connectionsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullConnectionsDesc,
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
return CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
final connection = connections[index];
|
||||
return ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClickKeyword: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(connection.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,40 +4,20 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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 Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.findProcessMode == FindProcessMode.always &&
|
||||
Platform.isAndroid,
|
||||
builder: (_, value, __) {
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -54,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,
|
||||
@@ -81,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,37 +1,31 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'item.dart';
|
||||
|
||||
double _preOffset = 0;
|
||||
|
||||
class RequestsFragment extends StatefulWidget {
|
||||
class RequestsFragment extends ConsumerStatefulWidget {
|
||||
const RequestsFragment({super.key});
|
||||
|
||||
@override
|
||||
State<RequestsFragment> createState() => _RequestsFragmentState();
|
||||
ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
|
||||
}
|
||||
|
||||
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
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;
|
||||
|
||||
@@ -51,26 +45,39 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final appController = globalState.appController;
|
||||
final appState = appController.appState;
|
||||
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||
connections: appState.requests,
|
||||
final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1;
|
||||
_scrollController = ScrollController(
|
||||
initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite,
|
||||
);
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
initViewState();
|
||||
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||
connections: globalState.appState.requests.list,
|
||||
);
|
||||
ref.listenManual(
|
||||
isCurrentPageProvider(
|
||||
PageLabel.requests,
|
||||
handler: (pageLabel, viewMode) =>
|
||||
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||
),
|
||||
(prev, next) {
|
||||
if (prev != next && next == true) {
|
||||
initPageState();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
ref.listenManual(
|
||||
requestsProvider.select((state) => state.list),
|
||||
(prev, next) {
|
||||
if (!connectionListEquality.equals(prev, next)) {
|
||||
_requests = next;
|
||||
updateRequestsThrottler();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
}
|
||||
|
||||
double _calcCacheHeight(Connection item) {
|
||||
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
|
||||
if (cacheHeight != null) {
|
||||
return cacheHeight;
|
||||
}
|
||||
final size = globalState.measure.computeTextSize(
|
||||
Text(
|
||||
item.desc,
|
||||
@@ -91,14 +98,13 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,26 +113,9 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
_requestsStateNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
_currentMaxWidth = 0;
|
||||
_cacheDynamicHeightMap.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _wrapPage(Widget child) {
|
||||
return Selector<AppState, bool?>(
|
||||
selector: (_, appState) =>
|
||||
appState.currentLabel == 'requests' ||
|
||||
appState.viewMode == ViewMode.mobile &&
|
||||
appState.currentLabel == "tools",
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent == null || isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
updateRequestsThrottler() {
|
||||
throttler.call("request", () {
|
||||
final isEquality = connectionListEquality.equals(
|
||||
@@ -144,95 +133,91 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
}, duration: commonDuration);
|
||||
}
|
||||
|
||||
Widget _wrapRequestsUpdate(Widget child) {
|
||||
return Selector<AppState, List<Connection>>(
|
||||
selector: (_, appState) => appState.requests,
|
||||
shouldRebuild: (prev, next) {
|
||||
final isEquality = connectionListEquality.equals(prev, next);
|
||||
if (!isEquality) {
|
||||
_requests = next;
|
||||
updateRequestsThrottler();
|
||||
}
|
||||
return !isEquality;
|
||||
},
|
||||
builder: (_, next, child) {
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
return FindProcessBuilder(builder: (value) {
|
||||
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
|
||||
return _wrapPage(
|
||||
_wrapRequestsUpdate(
|
||||
ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _requestsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullRequestsDesc,
|
||||
);
|
||||
}
|
||||
final items = connections
|
||||
.map<Widget>(
|
||||
(connection) => ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
),
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
},
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemExtentBuilder: (index, __) {
|
||||
final widget = items[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final measure = globalState.measure;
|
||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||
final connection = connections[(index / 2).floor()];
|
||||
final height = _calcCacheHeight(connection);
|
||||
return height + bodyMediumHeight + 32;
|
||||
},
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullRequestsDesc,
|
||||
);
|
||||
}
|
||||
final items = connections
|
||||
.map<Widget>(
|
||||
(connection) => ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClickKeyword: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
),
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ScrollToEndBox(
|
||||
controller: _scrollController,
|
||||
cacheKey: _cacheKey,
|
||||
dataSource: connections,
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: CacheItemExtentListView(
|
||||
key: _key,
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemExtentBuilder: (index) {
|
||||
final widget = items[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final measure = globalState.measure;
|
||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||
final connection = connections[(index / 2).floor()];
|
||||
final height = _calcCacheHeight(connection);
|
||||
return height + bodyMediumHeight + 32;
|
||||
},
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
keyBuilder: (int index) {
|
||||
final widget = items[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return "divider";
|
||||
}
|
||||
final connection = connections[(index / 2).floor()];
|
||||
return connection.id;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,31 +2,42 @@ import 'dart:math';
|
||||
|
||||
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:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'widgets/start_button.dart';
|
||||
|
||||
class DashboardFragment extends StatefulWidget {
|
||||
class DashboardFragment extends ConsumerStatefulWidget {
|
||||
const DashboardFragment({super.key});
|
||||
|
||||
@override
|
||||
State<DashboardFragment> createState() => _DashboardFragmentState();
|
||||
ConsumerState<DashboardFragment> createState() => _DashboardFragmentState();
|
||||
}
|
||||
|
||||
class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
class _DashboardFragmentState extends ConsumerState<DashboardFragment>
|
||||
with PageMixin {
|
||||
final key = GlobalKey<SuperGridState>();
|
||||
|
||||
_initScaffold(bool isCurrent) {
|
||||
if (!isCurrent) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.floatingActionButton = const StartButton();
|
||||
commonScaffoldState?.actions = [
|
||||
@override
|
||||
initState() {
|
||||
ref.listenManual(
|
||||
isCurrentPageProvider(PageLabel.dashboard),
|
||||
(prev, next) {
|
||||
if (prev != next && next == true) {
|
||||
initPageState();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
return super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get floatingActionButton => const StartButton();
|
||||
|
||||
@override
|
||||
List<Widget> get actions => [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: key.currentState!.addedChildrenNotifier,
|
||||
builder: (_, addedChildren, child) {
|
||||
@@ -67,72 +78,59 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
},
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
|
||||
final dashboardWidgets = girdItems
|
||||
.map(
|
||||
(item) => DashboardWidget.getDashboardWidget(item),
|
||||
)
|
||||
.toList();
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActiveBuilder(
|
||||
label: "dashboard",
|
||||
builder: (isCurrent, child) {
|
||||
_initScaffold(isCurrent);
|
||||
return child!;
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16).copyWith(
|
||||
bottom: 88,
|
||||
),
|
||||
child: Selector2<AppState, Config, DashboardState>(
|
||||
selector: (_, appState, config) => DashboardState(
|
||||
dashboardWidgets: config.appSetting.dashboardWidgets,
|
||||
viewWidth: appState.viewWidth,
|
||||
),
|
||||
builder: (_, state, ___) {
|
||||
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
|
||||
return SuperGrid(
|
||||
key: key,
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
children: [
|
||||
...state.dashboardWidgets
|
||||
.where(
|
||||
(item) => item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(item) => item.widget,
|
||||
final dashboardState = ref.watch(dashboardStateProvider);
|
||||
final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16).copyWith(
|
||||
bottom: 88,
|
||||
),
|
||||
child: SuperGrid(
|
||||
key: key,
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
children: [
|
||||
...dashboardState.dashboardWidgets
|
||||
.where(
|
||||
(item) => item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(item) => item.widget,
|
||||
),
|
||||
],
|
||||
onSave: (girdItems) {
|
||||
_handleSave(girdItems, ref);
|
||||
},
|
||||
addedItemsBuilder: (girdItems) {
|
||||
return DashboardWidget.values
|
||||
.where(
|
||||
(item) =>
|
||||
!girdItems.contains(item.widget) &&
|
||||
item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
],
|
||||
onSave: (girdItems) {
|
||||
final dashboardWidgets = girdItems
|
||||
.map(
|
||||
(item) => DashboardWidget.getDashboardWidget(item),
|
||||
)
|
||||
.toList();
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
dashboardWidgets: dashboardWidgets,
|
||||
);
|
||||
},
|
||||
addedItemsBuilder: (girdItems) {
|
||||
return DashboardWidget.values
|
||||
.where(
|
||||
(item) =>
|
||||
!girdItems.contains(item.widget) &&
|
||||
item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
)
|
||||
.map((item) => item.widget)
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.map((item) => item.widget)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user