Compare commits

..

11 Commits

Author SHA1 Message Date
chen08209
2fbc4170b9 Add some scenes auto close connections
Support proxies query

Optimize more details
2025-05-09 16:11:20 +08:00
chen08209
76c9f08d4a Fix issues that TUN repeat failed to open. 2025-05-01 22:12:05 +08:00
chen08209
f83a8e0cce Update changelog 2025-05-01 13:03:36 +00:00
chen08209
f5544f1af7 Fix windows service verify issues 2025-05-01 20:45:23 +08:00
chen08209
eeb543780a Update changelog 2025-04-30 16:20:40 +00:00
chen08209
676f2d058a Add windows server mode start process verify
Add linux deb dependencies

Add backup recovery strategy select

Support custom text scaling

Optimize the display of different text scale

Optimize windows setup experience

Optimize startTun performance

Optimize android tv experience

Optimize default option

Optimize computed text size

Optimize hyperOS freeform window

Add developer mode

Update core

Optimize more details
2025-05-01 00:02:29 +08:00
chen08209
fb9d0cb22c Add issues template 2025-04-19 22:57:07 +08:00
chen08209
e7eb312254 Update changelog 2025-04-18 09:09:33 +00:00
chen08209
c9cd80bcb3 Optimize android vpn performance
Add custom primary color and color scheme

Add linux nad windows arm release

Optimize requests and logs page
2025-04-18 16:54:05 +08:00
chen08209
a77b3a35e8 Fix map input page delete issues 2025-04-13 17:32:01 +08:00
chen08209
2d2708d7bd Update changelog 2025-04-08 07:55:45 +00:00
182 changed files with 9400 additions and 3020 deletions

57
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: 问题反馈 / Bug report
title: "[BUG] "
description: 反馈你遇到的问题 / Report the issue you are experiencing
body:
- type: markdown
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请务必给issue填写一个简洁明了的标题以便他人快速检索
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue否则请在已有的issue下进行讨论
3. 请务必按照模板规范详细描述问题否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
- type: textarea
id: description
attributes:
label: 问题描述 / Describe the bug
description: 详细清晰地描述你遇到的问题,并配合截图 / Describe the problem you encountered in detail and clearly, and provide screenshots
validations:
required: true
- type: textarea
attributes:
label: 软件版本 / Version
description: 请提供FlClash的具体版本 / Please provide the specific version of FlClash.
validations:
required: true
- type: textarea
attributes:
label: 复现步骤 / To Reproduce
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
validations:
required: true
- type: dropdown
attributes:
label: 操作系统 / OS
options:
- Android
- Windows
- MacOS
- Linux
validations:
required: true
- type: input
attributes:
label: 操作系统版本 / OS Version
description: 请提供你的操作系统版本Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
validations:
required: true
- type: textarea
attributes:
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
description: 请提供完整或相关部分的Debug日志请在“软件左侧菜单”->“设置”->“日志等级”调整到debug / Please provide a complete or relevant Debug log (please adjust it to debug in the left menu of software-> Settings-> Log Level)
validations:
required: true

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
contact_links:
- name: 讨论交流 / Communication
url: https://t.me/+G-veVtwBOl4wODc1
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group

View File

@@ -0,0 +1,42 @@
name: 功能请求 / Feature request
title: "[Feature] "
description: 提出你的功能请求 / Propose your feature request
body:
- type: markdown
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请务必给issue填写一个简洁明了的标题以便他人快速检索
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue否则请在已有的issue下进行讨论
3. 请务必按照模板规范详细描述问题否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
- type: textarea
id: description
attributes:
label: 功能描述 / Feature description
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
validations:
required: true
- type: textarea
attributes:
label: 使用场景 / Use case
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
validations:
required: true
- type: checkboxes
id: os-labels
attributes:
label: 适用系统 / Target OS
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
options:
- label: Android
- label: Windows
- label: MacOS
- label: Linux
validations:
required: true

View File

@@ -19,7 +19,7 @@ jobs:
os: windows-latest
arch: amd64
- platform: linux
os: ubuntu-latest
os: ubuntu-22.04
arch: amd64
- platform: macos
os: macos-13
@@ -27,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: |
@@ -58,18 +56,17 @@ jobs:
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
go-version: '1.24.0'
cache-dependency-path: |
core/go.sum
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
cache: true
- name: Get Flutter Dependency
@@ -204,6 +201,7 @@ jobs:
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TAG: ${{ github.ref_name }}
RUN_ID: ${{ github.run_id }}
run: |
python -m pip install --upgrade pip
pip install requests
@@ -214,6 +212,14 @@ jobs:
version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
- name: Generate sha256
if: env.IS_STABLE == 'true'
run: |
cd ./dist
for file in $(find . -type f -not -name "*.sha256"); do
sha256sum "$file" > "${file}.sha256"
done
- name: Release
if: ${{ env.IS_STABLE == 'true' }}
uses: softprops/action-gh-release@v2

4
.gitmodules vendored
View File

@@ -6,9 +6,5 @@
path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git
branch = FlClash
[submodule "plugins/tray_manager"]
path = plugins/tray_manager
url = git@github.com:chen08209/tray_manager.git
branch = main

View File

@@ -1,3 +1,67 @@
## v0.8.84
- Fix windows service verify issues
- Update changelog
## v0.8.83
- Add windows server mode start process verify
- Add linux deb dependencies
- Add backup recovery strategy select
- Support custom text scaling
- Optimize the display of different text scale
- Optimize windows setup experience
- Optimize startTun performance
- Optimize android tv experience
- Optimize default option
- Optimize computed text size
- Optimize hyperOS freeform window
- Add developer mode
- Update core
- Optimize more details
- Add issues template
- Update changelog
## v0.8.82
- Optimize android vpn performance
- Add custom primary color and color scheme
- Add linux nad windows arm release
- Optimize requests and logs page
- Fix map input page delete issues
- Update changelog
## v0.8.81
- Add rule override
- Update core
- Optimize more details
- Update changelog
## v0.8.80
- Optimize dashboard performance

10
Makefile Normal file
View File

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

View File

@@ -41,8 +41,8 @@ on Mobile:
⚠️ Make sure to install the following dependencies before using them
```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install libkeybinder-3.0-dev
```
### Android

View File

@@ -41,8 +41,8 @@ on Mobile:
⚠️ 使用前请确保安装以下依赖
```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install libkeybinder-3.0-dev
```
### Android

View File

@@ -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 35
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 {
@@ -74,6 +73,7 @@ android {
applicationIdSuffix '.debug'
}
release {
debuggable false
if (isRelease) {
signingConfig signingConfigs.release
} else {
@@ -84,31 +84,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
}
}

View File

@@ -1,13 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- The INTERNET permission is required for development. Specifically,
the 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>

View File

@@ -44,6 +44,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />

View File

@@ -20,6 +20,10 @@ enum class RunState {
object GlobalState {
val runLock = ReentrantLock()
const val NOTIFICATION_CHANNEL = "FlClash"
const val NOTIFICATION_ID = 1
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null
private var serviceEngine: FlutterEngine? = null

View File

@@ -27,7 +27,6 @@ import java.util.concurrent.locks.ReentrantLock
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun Drawable.getBase64(): String {
val drawable = this
return withContext(Dispatchers.IO) {

View File

@@ -3,6 +3,7 @@ package com.follow.clash.models
data class Package(
val packageName: String,
val label: String,
val isSystem: Boolean,
val system: Boolean,
val internet: Boolean,
val lastUpdateTime: Long,
)

View File

@@ -293,19 +293,17 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
if (packages.isNotEmpty()) return packages
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
?.filter {
it.packageName != FlClashApplication.getAppContext().packageName && (
it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
)
it.packageName != FlClashApplication.getAppContext().packageName || it.packageName == "android"
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo?.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
lastUpdateTime = it.lastUpdateTime
)
}?.let { packages.addAll(it) }
Package(
packageName = it.packageName,
label = it.applicationInfo?.loadLabel(packageManager).toString(),
system = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
lastUpdateTime = it.lastUpdateTime,
internet = it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
)
}?.let { packages.addAll(it) }
return packages
}

View File

@@ -52,7 +52,6 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.handleStop()
GlobalState.destroyServiceEngine()
}
}

View File

@@ -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,62 +93,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.success(true)
}
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null && flClashService is FlClashVpnService) {
try {
(flClashService as FlClashVpnService).protect(fd)
result.success(true)
} catch (e: RuntimeException) {
result.success(false)
}
} else {
result.success(false)
}
}
"resolverProcess" -> {
val data = call.argument<String>("data")
val process = if (data != null) Gson().fromJson(
data, Process::class.java
) else null
val metadata = process?.metadata
if (metadata == null) {
result.success(null)
return
}
val protocol = metadata.getProtocol()
if (protocol == null) {
result.success(null)
return
}
scope.launch {
withContext(Dispatchers.Default) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.success(null)
return@withContext
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
metadata.destinationIP.ifEmpty { metadata.host },
metadata.destinationPort
)
val uid = try {
connectivity?.getConnectionOwnerUid(protocol, src, dst)
} catch (_: Exception) {
null
}
if (uid == null || uid == -1) {
result.success(null)
return@withContext
}
val packages =
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
}
else -> {
result.notImplemented()
}
@@ -153,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()
@@ -162,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()
?.requestVpnPermission {
handleStartService()
}
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
handleStartService()
}
}
fun requestGc() {
@@ -219,8 +168,10 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
try {
if (GlobalState.runState.value != RunState.START) return
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
val startForegroundParams = Gson().fromJson(
val startForegroundParams = if (data != null) Gson().fromJson(
data, StartForegroundParams::class.java
) else StartForegroundParams(
title = "", content = ""
)
if (lastStartForegroundParams != startForegroundParams) {
lastStartForegroundParams = startForegroundParams
@@ -235,6 +186,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
private fun startForegroundJob() {
stopForegroundJob()
timerJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) {
startForeground()
@@ -256,26 +208,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)
}

View File

@@ -1,6 +1,26 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions
import io.flutter.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
interface BaseServiceInterface {
@@ -9,4 +29,70 @@ interface BaseServiceInterface {
fun stop()
suspend fun startForeground(title: String, content: String)
}
fun Service.createFlClashNotificationBuilder(): Deferred<NotificationCompat.Builder> =
CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val intent = Intent(this@createFlClashNotificationBuilder, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this@createFlClashNotificationBuilder,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this@createFlClashNotificationBuilder, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(
NotificationCompat.Builder(
this@createFlClashNotificationBuilder, GlobalState.NOTIFICATION_CHANNEL
)
) {
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
addAction(
0, stopText, getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
}
}
@SuppressLint("ForegroundServiceType")
fun Service.startForeground(notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
if (channel == null) {
Log.d("[FlClash]","createNotificationChannel===>")
channel = NotificationChannel(
GlobalState.NOTIFICATION_CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW
)
manager?.createNotificationChannel(channel)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
startForeground(
GlobalState.NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} catch (_: Exception) {
startForeground(GlobalState.NOTIFICATION_ID, notification)
}
} else {
startForeground(GlobalState.NOTIFICATION_ID, notification)
}
}

View File

@@ -1,29 +1,51 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
class FlClashService : Service(), BaseServiceInterface {
override fun start(options: VpnOptions) = 0
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
private var cachedBuilder: NotificationCompat.Builder? = null
private suspend fun notificationBuilder(): NotificationCompat.Builder {
if (cachedBuilder == null) {
cachedBuilder = createFlClashNotificationBuilder().await()
}
return cachedBuilder!!
}
@SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) {
startForeground(
notificationBuilder()
.setContentTitle(title)
.setContentText(content).build()
)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentVPNPlugin()?.requestGc()
}
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
@@ -38,93 +60,8 @@ class FlClashService : Service(), BaseServiceInterface {
return super.onUnbind(intent)
}
private val CHANNEL = "FlClash"
private val notificationId: Int = 1
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val intent = Intent(
this@FlClashService, MainActivity::class.java
)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this@FlClashService,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this@FlClashService,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this@FlClashService, CHANNEL)) {
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MIN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
addAction(
0,
stopText, // 使用 suspend 函数获取的文本
getActionPendingIntent("STOP")
)
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
}
}
}
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await()
}
override fun start(options: VpnOptions) = 0
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
@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)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
getNotificationBuilder()
.setContentTitle(title)
.setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification)
}
} else {
startForeground(notificationId, notification)
}
override fun onDestroy() {
stop()
super.onDestroy()
}
}

View File

@@ -1,12 +1,7 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Binder
@@ -17,18 +12,13 @@ import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.extensions.getIpv4RouteAddress
import com.follow.clash.extensions.getIpv6RouteAddress
import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@@ -43,6 +33,10 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
if (options.ipv4Address.isNotEmpty()) {
val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
Log.d(
"addAddress",
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
@@ -59,26 +53,39 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} else {
addRoute("0.0.0.0", 0)
}
} else {
addRoute("0.0.0.0", 0)
}
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
try {
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
Log.d(
"addAddress6",
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
}
} catch (_: Exception) {
} else {
addRoute("::", 0)
}
} else {
addRoute("::", 0)
}
}catch (_:Exception){
Log.d(
"addAddress6",
"IPv6 is not supported."
)
}
addDnsServer(options.dnsServerAddress)
setMtu(9000)
@@ -128,82 +135,22 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
}
}
private val CHANNEL = "FlClash"
private var cachedBuilder: NotificationCompat.Builder? = null
private val notificationId: Int = 1
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val intent = Intent(this@FlClashVpnService, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this@FlClashVpnService,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this@FlClashVpnService,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MIN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
addAction(
0,
stopText,
getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
}
private suspend fun notificationBuilder(): NotificationCompat.Builder {
if (cachedBuilder == null) {
cachedBuilder = createFlClashNotificationBuilder().await()
}
}
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await()
return cachedBuilder!!
}
@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)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
getNotificationBuilder()
startForeground(
notificationBuilder()
.setContentTitle(title)
.setContentText(content)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification)
}
} else {
startForeground(notificationId, notification)
}
.setContentText(content).build()
)
}
override fun onTrimMemory(level: Int) {

View File

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

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

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

View File

@@ -0,0 +1,66 @@
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.argumentsWithVarargAsSingleArray
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 {
isJniDebuggable = false
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"
}
}
kotlinOptions {
jvmTarget = "17"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("androidx.annotation:annotation-jvm:1.9.1")
}
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)
}
}

View File

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

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

View File

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

View File

@@ -0,0 +1,49 @@
cmake_minimum_required(VERSION 3.22.1)
project("core")
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
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
)
add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden)
endif ()
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")
message("LIB_CLASH_PATH ${LIB_CLASH_PATH}")
if (EXISTS ${LIB_CLASH_PATH})
message("Found libclash.so for ABI ${ANDROID_ABI}")
add_compile_definitions(LIBCLASH)
include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
clash)
else ()
message("Not found libclash.so for ABI ${ANDROID_ABI}")
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME})
endif ()

View File

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

View File

@@ -0,0 +1,71 @@
#include "jni_helper.h"
#include <cstdlib>
#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 = reinterpret_cast<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) {
const auto array = reinterpret_cast<jbyteArray>(env->CallObjectMethod(str, m_get_bytes));
const int length = env->GetArrayLength(array);
const auto content = static_cast<char *>(malloc(length + 1));
env->GetByteArrayRegion(array, 0, length, reinterpret_cast<jbyte *>(content));
content[length] = 0;
return content;
}
jstring jni_new_string(JNIEnv *env, const char *str) {
const auto length = static_cast<int>(strlen(str));
const auto array = env->NewByteArray(length);
env->SetByteArrayRegion(array, 0, length, reinterpret_cast<const jbyte *>(str));
return reinterpret_cast<jstring>(env->NewObject(c_string, m_new_string, array));
}
int jni_catch_exception(JNIEnv *env) {
const int result = env->ExceptionCheck();
if (result) {
env->ExceptionDescribe();
env->ExceptionClear();
}
return result;
}
void jni_attach_thread(scoped_jni *jni) {
JavaVM *vm = global_java_vm();
if (vm->GetEnv(reinterpret_cast<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(const scoped_jni *env) {
JavaVM *vm = global_java_vm();
if (env->require_release) {
vm->DetachCurrentThread();
}
}
void release_string(char **str) {
free(*str);
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <jni.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(scoped_jni *jni);
extern void jni_detach_thread(const scoped_jni *env);
extern void release_string(char **str);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
scoped_jni _jni{}; \
jni_attach_thread(&_jni); \
JNIEnv *env = _jni.env
#define scoped_string __attribute__((cleanup(release_string))) char*
#define find_class(name) env->FindClass(name)
#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature)
#define new_global(obj) env->NewGlobalRef(obj)
#define del_global(obj) env->DeleteGlobalRef(obj)
#define get_string(jstr) jni_get_string(env, jstr)
#define new_string(cstr) jni_new_string(env, cstr)

View File

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

View File

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

View File

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

View File

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

View File

@@ -372,5 +372,33 @@
"generalDesc": "Modify general settings",
"findProcessModeDesc": "There is a certain performance loss after opening",
"tabAnimationDesc": "Effective only in mobile view",
"saveTip": "Are you sure you want to save?"
"saveTip": "Are you sure you want to save?",
"deleteColorTip": "Are you sure you want to delete the current color?",
"colorExists": "Current color already exists",
"colorSchemes": "Color schemes",
"palette": "Palette",
"tonalSpotScheme": "TonalSpot",
"fidelityScheme": "Fidelity",
"monochromeScheme": "Monochrome",
"neutralScheme": "Neutral",
"vibrantScheme": "Vibrant",
"expressiveScheme": "Expressive",
"contentScheme": "Content",
"rainbowScheme": "Rainbow",
"fruitSaladScheme": "FruitSalad",
"developerMode": "Developer mode",
"developerModeEnableTip": "Developer mode is enabled.",
"messageTest": "Message test",
"messageTestTip": "This is a message.",
"crashTest": "Crash test",
"clearData": "Clear Data",
"textScale": "Text Scaling",
"internet": "Internet",
"systemApp": "System APP",
"noNetworkApp": "No network APP",
"contactMe": "Contact me",
"recoveryStrategy": "Recovery strategy",
"recoveryStrategy_override": "Override",
"recoveryStrategy_compatible": "Compatible",
"logsTest": "Logs test"
}

View File

@@ -372,5 +372,34 @@
"generalDesc": "一般設定を変更",
"findProcessModeDesc": "有効化するとパフォーマンスが若干低下します",
"tabAnimationDesc": "モバイル表示でのみ有効",
"saveTip": "保存してもよろしいですか?"
"saveTip": "保存してもよろしいですか?",
"deleteColorTip": "現在の色を削除しますか?",
"colorExists": "この色は既に存在します",
"colorSchemes": "カラースキーム",
"palette": "パレット",
"tonalSpotScheme": "トーンスポット",
"fidelityScheme": "ハイファイデリティー",
"monochromeScheme": "モノクローム",
"neutralScheme": "ニュートラル",
"vibrantScheme": "ビブラント",
"expressiveScheme": "エクスプレッシブ",
"contentScheme": "コンテンツテーマ",
"rainbowScheme": "レインボー",
"fruitSaladScheme": "フルーツサラダ",
"developerMode": "デベロッパーモード",
"developerModeEnableTip": "デベロッパーモードが有効になりました。",
"messageTest": "メッセージテスト",
"messageTestTip": "これはメッセージです。",
"crashTest": "クラッシュテスト",
"clearData": "データを消去",
"zoom": "ズーム",
"textScale": "テキストスケーリング",
"internet": "インターネット",
"systemApp": "システムアプリ",
"noNetworkApp": "ネットワークなしアプリ",
"contactMe": "連絡する",
"recoveryStrategy": "リカバリー戦略",
"recoveryStrategy_override": "オーバーライド",
"recoveryStrategy_compatible": "互換性",
"logsTest": "ログテスト"
}

View File

@@ -372,5 +372,34 @@
"generalDesc": "Изменение общих настроек",
"findProcessModeDesc": "При включении возможны небольшие потери производительности",
"tabAnimationDesc": "Действительно только в мобильном виде",
"saveTip": "Вы уверены, что хотите сохранить?"
"saveTip": "Вы уверены, что хотите сохранить?",
"deleteColorTip": "Удалить текущий цвет?",
"colorExists": "Этот цвет уже существует",
"colorSchemes": "Цветовые схемы",
"palette": "Палитра",
"tonalSpotScheme": "Тональный акцент",
"fidelityScheme": "Точная передача",
"monochromeScheme": "Монохром",
"neutralScheme": "Нейтральные",
"vibrantScheme": "Яркие",
"expressiveScheme": "Экспрессивные",
"contentScheme": "Контентная тема",
"rainbowScheme": "Радужные",
"fruitSaladScheme": "Фруктовый микс",
"developerMode": "Режим разработчика",
"developerModeEnableTip": "Режим разработчика активирован.",
"messageTest": "Тестирование сообщения",
"messageTestTip": "Это сообщение.",
"crashTest": "Тест на сбои",
"clearData": "Очистить данные",
"zoom": "Масштаб",
"textScale": "Масштабирование текста",
"internet": "Интернет",
"systemApp": "Системное приложение",
"noNetworkApp": "Приложение без сети",
"contactMe": "Свяжитесь со мной",
"recoveryStrategy": "Стратегия восстановления",
"recoveryStrategy_override": "Переопределение",
"recoveryStrategy_compatible": "Совместимый",
"logsTest": "Тест журналов"
}

View File

@@ -372,5 +372,34 @@
"generalDesc": "修改通用设置",
"findProcessModeDesc": "开启后会有一定性能损耗",
"tabAnimationDesc": "仅在移动视图中有效",
"saveTip": "确定要保存吗?"
"saveTip": "确定要保存吗?",
"deleteColorTip": "确定删除当前颜色吗?",
"colorExists": "该颜色已存在",
"colorSchemes": "配色方案",
"palette": "调色板",
"tonalSpotScheme": "调性点缀",
"fidelityScheme": "高保真",
"monochromeScheme": "单色",
"neutralScheme": "中性",
"vibrantScheme": "活力",
"expressiveScheme": "表现力",
"contentScheme": "内容主题",
"rainbowScheme": "彩虹",
"fruitSaladScheme": "果缤纷",
"developerMode": "开发者模式",
"developerModeEnableTip": "开发者模式已启用。",
"messageTest": "消息测试",
"messageTestTip": "这是一条消息。",
"crashTest": "崩溃测试",
"clearData": "清除数据",
"zoom": "缩放",
"textScale": "文本缩放",
"internet": "互联网",
"systemApp": "系统应用",
"noNetworkApp": "无网络应用",
"contactMe": "联系我",
"recoveryStrategy": "恢复策略",
"recoveryStrategy_override": "覆盖",
"recoveryStrategy_compatible": "兼容",
"logsTest": "日志测试"
}

View File

@@ -35,8 +35,8 @@ func (action Action) getResult(data interface{}) []byte {
func handleAction(action *Action, result func(data interface{})) {
switch action.Method {
case initClashMethod:
data := action.Data.(string)
result(handleInitClash(data))
paramsString := action.Data.(string)
result(handleInitClash(paramsString))
return
case getIsInitMethod:
result(handleGetIsInit())
@@ -166,6 +166,9 @@ func handleAction(action *Action, result func(data interface{})) {
data := action.Data.(string)
handleSetState(data)
result(true)
case crashMethod:
result(true)
handleCrash()
default:
handle := nextHandle(action, result)
if handle {

77
core/android_bride.go Normal file
View File

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

View File

@@ -41,6 +41,7 @@ func splitByMultipleSeparators(s string) interface{} {
}
var (
version = 0
isRunning = false
runLock sync.Mutex
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
@@ -77,7 +78,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
path := getProfilePath(id)
bytes, err := readFile(path)
if err != nil {
log.Errorln("profile is not exist")
return config.DefaultRawConfig()
}
prof, err := config.UnmarshalRawConfig(bytes)
@@ -274,7 +274,6 @@ func patchConfig() {
dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay)
tunnel.SetMode(general.Mode)
tunnel.UpdateRules(currentConfig.Rules, currentConfig.SubRules, currentConfig.RuleProviders)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6

View File

@@ -7,6 +7,11 @@ 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"`
@@ -71,16 +76,13 @@ 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"
crashMethod Method = "crash"
)
type Method string
@@ -109,20 +111,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)
}

View File

@@ -7,6 +7,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.49.1
golang.org/x/sync v0.11.0
)
require (
@@ -52,20 +53,20 @@ require (
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.19.0 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.1 // indirect
github.com/metacubex/chacha v0.1.2 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 // 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.8-alpha.4 // indirect
github.com/metacubex/utls v1.7.0-alpha.1 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
@@ -84,7 +85,6 @@ require (
github.com/sagernet/nftables v0.3.0-beta.4 // 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.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
@@ -107,7 +107,6 @@ require (
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect

View File

@@ -101,8 +101,8 @@ github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
@@ -111,24 +111,24 @@ github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/j
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/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c h1:OB3WmMA8YPJjE36RjD9X8xlrWGJ4orxbf2R/KAE28b0=
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 h1:vy/8ZYYtWUXYnOnw/NF8ThG1W/RqM/h5rkun+OXZMH0=
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63/go.mod h1:eDZ2JpkSkewGmUlCoLSn2MRFn1D0jKPIys/6aogFx7U=
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 h1:hcsz5e5lqhBxn3iQQDIF60FLZ8PQT542GTQZ+1wcIGo=
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5/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.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI=
github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
github.com/metacubex/utls v1.7.0-alpha.1 h1:oMFsPh2oTlALJ7vKXPJuqgy0YeiZ+q/LLw+ZdxZ80l4=
github.com/metacubex/utls v1.7.0-alpha.1/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
@@ -174,8 +174,6 @@ 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.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=

View File

@@ -34,9 +34,15 @@ var (
currentConfig *config.Config
)
func handleInitClash(homeDirStr string) bool {
func handleInitClash(paramsString string) bool {
var params = InitParams{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
return false
}
version = params.Version
if !isInit {
constant.SetHomeDir(homeDirStr)
constant.SetHomeDir(params.HomeDir)
isInit = true
}
return isInit
@@ -47,6 +53,7 @@ func handleStartListener() bool {
defer runLock.Unlock()
isRunning = true
updateListeners(true)
closeConnections()
return true
}
@@ -270,6 +277,16 @@ func handleCloseConnections() bool {
return true
}
func closeConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close()
if err != nil {
return false
}
return true
})
}
func handleCloseConnection(connectionId string) bool {
runLock.Lock()
defer runLock.Unlock()
@@ -436,6 +453,10 @@ func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func handleCrash() {
panic("handle invoke crash")
}
func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{

View File

@@ -4,6 +4,7 @@ package main
import "C"
import (
"context"
bridge "core/dart-bridge"
"core/platform"
"core/state"
@@ -11,118 +12,111 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"net"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unsafe"
)
type Fd struct {
Id string `json:"id"`
Value int64 `json:"value"`
type TunHandler struct {
listener *sing_tun.Listener
callback unsafe.Pointer
limit *semaphore.Weighted
}
type Process struct {
Id string `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
type ProcessMapItem struct {
Id string `json:"id"`
Value string `json:"value"`
}
type InvokeManager struct {
invokeMap sync.Map
chanMap map[string]chan struct{}
chanLock sync.Mutex
}
func NewInvokeManager() *InvokeManager {
return &InvokeManager{
chanMap: make(map[string]chan struct{}),
func (t *TunHandler) close() {
_ = t.limit.Acquire(context.TODO(), 4)
defer t.limit.Release(4)
removeTunHook()
if t.listener != nil {
_ = t.listener.Close()
}
if t.callback != nil {
releaseObject(t.callback)
}
t.callback = nil
t.listener = nil
}
func (m *InvokeManager) completer(id string, value string) {
m.invokeMap.Store(id, value)
m.chanLock.Lock()
if ch, ok := m.chanMap[id]; ok {
close(ch)
delete(m.chanMap, id)
func (t *TunHandler) handleProtect(fd int) {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.listener == nil {
return
}
m.chanLock.Unlock()
protect(t.callback, fd)
}
func (m *InvokeManager) await(id string) string {
m.chanLock.Lock()
if _, ok := m.chanMap[id]; !ok {
m.chanMap[id] = make(chan struct{})
}
ch := m.chanMap[id]
m.chanLock.Unlock()
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
timeout := time.After(500 * time.Millisecond)
select {
case <-ch:
res, ok := m.invokeMap.Load(id)
m.invokeMap.Delete(id)
if ok {
return res.(string)
} else {
return ""
}
case <-timeout:
m.completer(id, "")
if t.listener == nil {
return ""
}
var protocol int
uid := -1
switch source.Network() {
case "udp", "udp4", "udp6":
protocol = syscall.IPPROTO_UDP
case "tcp", "tcp4", "tcp6":
protocol = syscall.IPPROTO_TCP
}
if version < 29 {
uid = platform.QuerySocketUidFromProcFs(source, target)
}
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
}
var (
invokePort int64 = -1
tunListener *sing_tun.Listener
fdInvokeMap = NewInvokeManager()
processInvokeMap = NewInvokeManager()
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
tunHandler *TunHandler
)
func handleStartTun(fd int) string {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
if fd == 0 {
now := time.Now()
runTime = &now
} else {
initSocketHook()
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
}
now := time.Now()
runTime = &now
}
return handleGetRunTime()
}
func handleStopTun() {
tunLock.Lock()
defer tunLock.Unlock()
removeSocketHook()
runTime = nil
if tunListener != nil {
log.Infoln("TUN close")
_ = tunListener.Close()
if tunHandler != nil {
tunHandler.close()
}
}
func handleStartTun(fd int, callback unsafe.Pointer) {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
if fd != 0 {
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())
tunHandler.listener = tunListener
} else {
removeTunHook()
}
}
}
@@ -133,83 +127,29 @@ func handleGetRunTime() string {
return strconv.FormatInt(runTime.UnixMilli(), 10)
}
func handleSetProcessMap(params string) {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(params), processMapItem)
if err == nil {
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
}
}
//export attachInvokePort
func attachInvokePort(mPort C.longlong) {
invokePort = int64(mPort)
}
func sendInvokeMessage(message InvokeMessage) {
if invokePort == -1 {
return
}
bridge.SendToPort(invokePort, message.Json())
}
func handleMarkSocket(fd Fd) {
sendInvokeMessage(InvokeMessage{
Type: ProtectInvoke,
Data: fd,
})
}
func handleParseProcess(process Process) {
sendInvokeMessage(InvokeMessage{
Type: ProcessInvoke,
Data: process,
})
}
func handleSetFdMap(id string) {
go func() {
fdInvokeMap.completer(id, "")
}()
}
func initSocketHook() {
func initTunHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
fdInt := int64(fd)
id := utils.NewUUIDV1().String()
handleMarkSocket(Fd{
Id: id,
Value: fdInt,
})
fdInvokeMap.await(id)
tunHandler.handleProtect(int(fd))
})
}
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil {
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork
}
id := utils.NewUUIDV1().String()
handleParseProcess(Process{
Id: id,
Metadata: metadata,
})
return processInvokeMap.await(id), nil
return tunHandler.handleResolveProcess(src, dst), nil
}
}
func removeTunHook() {
dialer.DefaultSocketHook = nil
process.DefaultPackageNameResolver = nil
}
func handleGetAndroidVpnOptions() string {
tunLock.Lock()
defer tunLock.Unlock()
@@ -250,16 +190,6 @@ func handleGetCurrentProfileName() string {
func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method {
case startTunMethod:
data := action.Data.(string)
var fd int
_ = json.Unmarshal([]byte(data), &fd)
result(handleStartTun(fd))
return true
case stopTunMethod:
handleStopTun()
result(true)
return true
case getAndroidVpnOptionsMethod:
result(handleGetAndroidVpnOptions())
return true
@@ -268,16 +198,6 @@ func nextHandle(action *Action, result func(data interface{})) bool {
handleUpdateDns(data)
result(true)
return true
case setFdMapMethod:
fdId := action.Data.(string)
handleSetFdMap(fdId)
result(true)
return true
case setProcessMapMethod:
data := action.Data.(string)
handleSetProcessMap(data)
result(true)
return true
case getRunTimeMethod:
result(handleGetRunTime())
return true
@@ -289,13 +209,13 @@ func nextHandle(action *Action, result func(data interface{})) bool {
}
//export quickStart
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port)
dir := C.GoString(dirChar)
paramsString := C.GoString(initParamsChar)
bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar)
go func() {
res := handleInitClash(dir)
res := handleInitClash(paramsString)
if res == false {
bridge.SendToPort(i, "init error")
}
@@ -305,9 +225,11 @@ 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 {
go func() {
handleStartTun(int(fd), callback)
}()
return true
}
//export getRunTime
@@ -317,13 +239,9 @@ func getRunTime() *C.char {
//export stopTun
func stopTun() {
handleStopTun()
}
//export setFdMap
func setFdMap(fdIdChar *C.char) {
fdId := C.GoString(fdIdChar)
handleSetFdMap(fdId)
go func() {
handleStopTun()
}()
}
//export getCurrentProfileName
@@ -347,12 +265,3 @@ func updateDns(s *C.char) {
dnsList := C.GoString(s)
handleUpdateDns(dnsList)
}
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
handleSetProcessMap(paramsString)
}

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

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

View File

@@ -24,7 +24,6 @@ type AccessControl struct {
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
IsFilterSystemApp bool `json:"isFilterSystemApp"`
}
type AndroidVpnRawOptions struct {

View File

@@ -1,20 +1,18 @@
import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'controller.dart';
import 'models/models.dart';
import 'pages/pages.dart';
class Application extends ConsumerStatefulWidget {
@@ -27,7 +25,6 @@ class Application extends ConsumerStatefulWidget {
}
class ApplicationState extends ConsumerState<Application> {
late ColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer;
@@ -43,16 +40,8 @@ class ApplicationState extends ConsumerState<Application> {
ColorScheme _getAppColorScheme({
required Brightness brightness,
int? primaryColor,
required ColorSchemes systemColorSchemes,
}) {
if (primaryColor != null) {
return ColorScheme.fromSeed(
seedColor: Color(primaryColor),
brightness: brightness,
);
} else {
return systemColorSchemes.getColorSchemeForBrightness(brightness);
}
return ref.read(genColorSchemeProvider(brightness));
}
@override
@@ -111,7 +100,8 @@ class ApplicationState extends ConsumerState<Application> {
return AppStateManager(
child: ClashManager(
child: ConnectivityManager(
onConnectivityChanged: () {
onConnectivityChanged: () async {
await clashCore.closeConnections();
globalState.appController.updateLocalIp();
globalState.appController.addCheckIpNumDebounce();
},
@@ -140,19 +130,6 @@ class ApplicationState extends ConsumerState<Application> {
);
}
_updateSystemColorSchemes(
ColorScheme? lightDynamic,
ColorScheme? darkDynamic,
) {
systemColorSchemes = ColorSchemes(
lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateSystemColorSchemes(systemColorSchemes);
});
}
@override
Widget build(context) {
return _buildPlatformState(
@@ -162,51 +139,44 @@ class ApplicationState extends ConsumerState<Application> {
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,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return AppEnvManager(
child: _buildPlatformApp(
_buildApp(child!),
),
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: themeProps.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: themeProps.primaryColor,
),
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return AppEnvManager(
child: _buildPlatformApp(
_buildApp(child!),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: themeProps.primaryColor,
).toPureBlack(themeProps.pureBlack),
),
home: child,
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: utils.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: themeProps.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
primaryColor: themeProps.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
primaryColor: themeProps.primaryColor,
).toPureBlack(themeProps.pureBlack),
),
home: child,
);
},
child: const HomePage(),

View File

@@ -8,6 +8,7 @@ import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
@@ -66,7 +67,12 @@ class ClashCore {
Future<bool> init() async {
await initGeo();
final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(homeDirPath);
return await clashInterface.init(
InitParams(
homeDir: homeDirPath,
version: globalState.appState.version,
),
);
}
Future<bool> setState(CoreState state) async {

View File

@@ -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;

View File

@@ -7,7 +7,7 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
mixin ClashInterface {
Future<bool> init(String homeDir);
Future<bool> init(InitParams params);
Future<bool> preload();
@@ -58,6 +58,8 @@ mixin ClashInterface {
stopLog();
Future<bool> crash();
FutureOr<String> getConnections();
FutureOr<bool> closeConnection(String id);
@@ -74,12 +76,10 @@ mixin AndroidClashInterface {
Future<bool> setProcessMap(ProcessMapItem item);
Future<bool> stopTun();
// Future<bool> stopTun();
Future<bool> updateDns(String value);
Future<DateTime?> startTun(int fd);
Future<AndroidVpnOptions?> getAndroidVpnOptions();
Future<String> getCurrentProfileName();
@@ -106,6 +106,7 @@ abstract class ClashHandlerInterface with ClashInterface {
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
case ActionMethod.setState:
case ActionMethod.crash:
completer?.complete(result.data as bool);
return;
case ActionMethod.changeProxy:
@@ -153,7 +154,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 +192,10 @@ abstract class ClashHandlerInterface with ClashInterface {
}
@override
Future<bool> init(String homeDir) {
Future<bool> init(InitParams params) {
return invoke<bool>(
method: ActionMethod.initClash,
data: homeDir,
data: json.encode(params),
);
}
@@ -244,6 +245,13 @@ abstract class ClashHandlerInterface with ClashInterface {
);
}
@override
Future<bool> crash() {
return invoke<bool>(
method: ActionMethod.crash,
);
}
@override
Future<String> getProxies() {
return invoke<String>(

View File

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

View File

@@ -71,9 +71,9 @@ class ClashService extends ClashHandlerInterface {
}
}, (error, stack) {
commonPrint.log(error.toString());
if(error is SocketException){
if (error is SocketException) {
globalState.showNotifier(error.toString());
globalState.appController.restartCore();
// globalState.appController.restartCore();
}
});
}
@@ -92,12 +92,11 @@ class ClashService extends ClashHandlerInterface {
final arg = Platform.isWindows
? "${serverSocket.port}"
: serverSocket.address.address;
bool isSuccess = false;
if (Platform.isWindows && await system.checkIsAdmin()) {
isSuccess = await request.startCoreByHelper(arg);
}
if (isSuccess) {
return;
final isSuccess = await request.startCoreByHelper(arg);
if (isSuccess) {
return;
}
}
process = await Process.start(
appPath.corePath,

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
extension ColorExtension on Color {
@@ -37,11 +39,54 @@ extension ColorExtension on Color {
return withAlpha(0);
}
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();
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(
@@ -74,7 +119,7 @@ extension ColorSchemeExtension on ColorScheme {
? copyWith(
surface: Colors.black,
surfaceContainer: surfaceContainer.darken(
0.05,
5,
),
)
: this;

View File

@@ -12,14 +12,14 @@ export 'iterable.dart';
export 'keyboard.dart';
export 'launch.dart';
export 'link.dart';
export 'list.dart';
export 'fixed.dart';
export 'lock.dart';
export 'measure.dart';
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';
@@ -36,4 +36,4 @@ export 'window.dart';
export 'windows.dart';
export 'render.dart';
export 'mixin.dart';
export 'print.dart';
export 'print.dart';

View File

@@ -16,16 +16,15 @@ const browserUa =
const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
const helperTag = "2024125";
const baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
const maxTextScale = 1.4;
const minTextScale = 0.8;
final baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16.ap,
horizontal: 16.ap,
);
double textScaleFactor = min(
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
1.2,
);
final defaultTextScaleFactor =
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
@@ -44,7 +43,6 @@ const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
const clashConfigKey = "clash_config";
const configKey = "config";
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
const double dialogCommonWidth = 300;
const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090";
@@ -60,6 +58,7 @@ final commonFilter = ImageFilter.blur(
const navigationItemListEquality = ListEquality<NavigationItem>();
const connectionListEquality = ListEquality<Connection>();
const stringListEquality = ListEquality<String>();
const intListEquality = ListEquality<int>();
const logListEquality = ListEquality<Log>();
const groupListEquality = ListEquality<Group>();
const externalProviderListEquality = ListEquality<ExternalProvider>();
@@ -78,12 +77,28 @@ const viewModeColumnsMap = {
ViewMode.desktop: [4, 3],
};
const defaultPrimaryColor = Colors.brown;
// const proxiesStoreKey = PageStorageKey<String>('proxies');
// const toolsStoreKey = PageStorageKey<String>('tools');
// const profilesStoreKey = PageStorageKey<String>('profiles');
const defaultPrimaryColor = 0XFFD8C0C3;
double getWidgetHeight(num lines) {
return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0);
return max(lines * 84 + (lines - 1) * 16, 0).ap;
}
const maxLength = 150;
final mainIsolate = "FlClashMainIsolate";
final serviceIsolate = "FlClashServiceIsolate";
const defaultPrimaryColors = [
0xFF795548,
0xFF03A9F4,
0xFFFFFF00,
0XFFBBC9CC,
0XFFABD397,
defaultPrimaryColor,
0XFF665390,
];

View File

@@ -1,4 +1,4 @@
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/manager/message_manager.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
@@ -11,6 +11,36 @@ extension BuildContextExtension on BuildContext {
return findAncestorStateOfType<MessageManagerState>()?.message(text);
}
showSnackBar(
String message, {
SnackBarAction? action,
}) {
final width = viewWidth;
EdgeInsets margin;
if (width < 600) {
margin = const EdgeInsets.only(
bottom: 16,
right: 16,
left: 16,
);
} else {
margin = EdgeInsets.only(
bottom: 16,
left: 16,
right: width - 316,
);
}
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
action: action,
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(milliseconds: 1500),
margin: margin,
),
);
}
Size get appSize {
return MediaQuery.of(this).size;
}
@@ -27,10 +57,10 @@ extension BuildContextExtension on BuildContext {
T? state;
visitor(Element element) {
if(!element.mounted){
if (!element.mounted) {
return;
}
if(element is StatefulElement){
if (element is StatefulElement) {
if (element.state is T) {
state = element.state as T;
}

79
lib/common/fixed.dart Normal file
View File

@@ -0,0 +1,79 @@
import 'iterable.dart';
class FixedList<T> {
final int maxLength;
final List<T> _list;
FixedList(this.maxLength, {List<T>? list})
: _list = (list ?? [])..truncate(maxLength);
add(T item) {
_list.add(item);
_list.truncate(maxLength);
}
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
class FixedMap<K, V> {
int maxLength;
late Map<K, V> _map;
FixedMap(this.maxLength, {Map<K, V>? map}) {
_map = map ?? {};
}
updateCacheValue(K key, V Function() callback) {
final realValue = _map.updateCacheValue(
key,
callback,
);
_adjustMap();
return realValue;
}
clear() {
_map.clear();
}
updateMaxLength(int size) {
maxLength = size;
_adjustMap();
}
updateMap(Map<K, V> map) {
_map = map;
_adjustMap();
}
_adjustMap() {
if (_map.length > maxLength) {
_map = Map.fromEntries(
map.entries.toList()..truncate(maxLength),
);
}
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;
Map<K, V> get map => Map.unmodifiable(_map);
}

View File

@@ -1,10 +1,12 @@
import 'dart:async';
import 'package:fl_clash/enum/enum.dart';
class Debouncer {
final Map<dynamic, Timer?> _operations = {};
final Map<FunctionTag, Timer?> _operations = {};
call(
dynamic tag,
FunctionTag tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
@@ -33,10 +35,10 @@ class Debouncer {
}
class Throttler {
final Map<dynamic, Timer?> _operations = {};
final Map<FunctionTag, Timer?> _operations = {};
call(
dynamic tag,
FunctionTag tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),

View File

@@ -38,6 +38,43 @@ extension IterableExt<T> on Iterable<T> {
count++;
}
}
Iterable<T> takeLast({int count = 50}) {
if (count <= 0) return Iterable.empty();
return count >= length ? this : toList().skip(length - count);
}
}
extension ListExt<T> on List<T> {
void truncate(int maxLength) {
assert(maxLength > 0);
if (length > maxLength) {
removeRange(0, length - maxLength);
}
}
List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList();
}
List<List<T>> batch(int maxConcurrent) {
final batches = (length / maxConcurrent).ceil();
final List<List<T>> res = [];
for (int i = 0; i < batches; i++) {
if (i != batches - 1) {
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
} else {
res.add(sublist(i * maxConcurrent, length));
}
}
return res;
}
List<T> safeSublist(int start) {
if (start <= 0) return this;
if (start > length) return [];
return sublist(start);
}
}
extension DoubleListExt on List<double> {
@@ -67,9 +104,9 @@ extension DoubleListExt on List<double> {
}
extension MapExt<K, V> on Map<K, V> {
getCacheValue(K key, V defaultValue) {
updateCacheValue(K key, V Function() callback) {
if (this[key] == null) {
this[key] = defaultValue;
this[key] = callback();
}
return this[key];
}

View File

@@ -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();

View File

@@ -1,93 +0,0 @@
import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list;
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
add(T item) {
if (_list.length == maxLength) {
_list.removeAt(0);
}
_list.add(item);
}
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
class FixedMap<K, V> {
int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
FixedMap(this.maxSize);
put(K key, V value) {
if (_map.length == maxSize) {
final oldestKey = _queue.removeFirst();
_map.remove(oldestKey);
}
_map[key] = value;
_queue.add(key);
return value;
}
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;
Map<K, V> get map => Map.unmodifiable(_map);
}
extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList();
}
List<List<T>> batch(int maxConcurrent) {
final batches = (length / maxConcurrent).ceil();
final List<List<T>> res = [];
for (int i = 0; i < batches; i++) {
if (i != batches - 1) {
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
} else {
res.add(sublist(i * maxConcurrent, length));
}
}
return res;
}
List<T> safeSublist(int start) {
if (start <= 0) return this;
if (start > length) return [];
return sublist(start);
}
}

View File

@@ -3,11 +3,13 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class Measure {
final TextScaler _textScale;
final TextScaler _textScaler;
final BuildContext context;
final Map<String, dynamic> _measureMap;
Measure.of(this.context)
: _textScale = TextScaler.linear(
Measure.of(this.context, double textScaleFactor)
: _measureMap = {},
_textScaler = TextScaler.linear(
textScaleFactor,
);
@@ -21,7 +23,7 @@ class Measure {
style: text.style,
),
maxLines: text.maxLines,
textScaler: _textScale,
textScaler: _textScaler,
textDirection: text.textDirection ?? TextDirection.ltr,
)..layout(
maxWidth: maxWidth,
@@ -29,81 +31,87 @@ class Measure {
return textPainter.size;
}
double? _bodyMediumHeight;
Size? _bodyLargeSize;
double? _bodySmallHeight;
double? _labelSmallHeight;
double? _labelMediumHeight;
double? _titleLargeHeight;
double? _titleMediumHeight;
double get bodyMediumHeight {
_bodyMediumHeight ??= computeTextSize(
Text(
"X",
style: context.textTheme.bodyMedium,
),
).height;
return _bodyMediumHeight!;
return _measureMap.updateCacheValue(
"bodyMediumHeight",
() => computeTextSize(
Text(
"X",
style: context.textTheme.bodyMedium,
),
).height,
);
}
Size get bodyLargeSize {
_bodyLargeSize ??= computeTextSize(
Text(
"X",
style: context.textTheme.bodyLarge,
),
double get bodyLargeHeight {
return _measureMap.updateCacheValue(
"bodyLargeHeight",
() => computeTextSize(
Text(
"X",
style: context.textTheme.bodyLarge,
),
).height,
);
return _bodyLargeSize!;
}
double get bodySmallHeight {
_bodySmallHeight ??= computeTextSize(
Text(
"X",
style: context.textTheme.bodySmall,
),
).height;
return _bodySmallHeight!;
return _measureMap.updateCacheValue(
"bodySmallHeight",
() => computeTextSize(
Text(
"X",
style: context.textTheme.bodySmall,
),
).height,
);
}
double get labelSmallHeight {
_labelSmallHeight ??= computeTextSize(
Text(
"X",
style: context.textTheme.labelSmall,
),
).height;
return _labelSmallHeight!;
return _measureMap.updateCacheValue(
"labelSmallHeight",
() => computeTextSize(
Text(
"X",
style: context.textTheme.labelSmall,
),
).height,
);
}
double get labelMediumHeight {
_labelMediumHeight ??= computeTextSize(
Text(
"X",
style: context.textTheme.labelMedium,
),
).height;
return _labelMediumHeight!;
return _measureMap.updateCacheValue(
"labelMediumHeight",
() => computeTextSize(
Text(
"X",
style: context.textTheme.labelMedium,
),
).height,
);
}
double get titleLargeHeight {
_titleLargeHeight ??= computeTextSize(
Text(
"X",
style: context.textTheme.titleLarge,
),
).height;
return _titleLargeHeight!;
return _measureMap.updateCacheValue(
"titleLargeHeight",
() => computeTextSize(
Text(
"X",
style: context.textTheme.titleLarge,
),
).height,
);
}
double get titleMediumHeight {
_titleMediumHeight ??= computeTextSize(
Text(
"X",
style: context.textTheme.titleMedium,
),
).height;
return _titleMediumHeight!;
return _measureMap.updateCacheValue(
"titleMediumHeight",
() => computeTextSize(
Text(
"X",
style: context.textTheme.titleMedium,
),
).height,
);
}
}

View File

@@ -12,9 +12,9 @@ class Navigation {
}) {
return [
const NavigationItem(
keep: false,
icon: Icon(Icons.space_dashboard),
label: PageLabel.dashboard,
keep: false,
fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard),
),
@@ -66,7 +66,6 @@ class Navigation {
icon: Icon(Icons.storage),
label: PageLabel.resources,
description: "resourcesDesc",
keep: false,
fragment: Resources(
key: GlobalObjectKey(
PageLabel.resources,

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/state.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -12,6 +13,10 @@ extension NumExt on num {
}
return formatted;
}
double get ap {
return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5);
}
}
extension DoubleExt on double {

View File

@@ -1,4 +1,3 @@
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';
@@ -20,10 +19,7 @@ class CommonPrint {
return;
}
globalState.appController.addLog(
Log(
logLevel: LogLevel.info,
payload: payload,
),
Log.app(payload),
);
}
}

View File

@@ -23,14 +23,14 @@ class Render {
pause() {
throttler.call(
DebounceTag.renderPause,
FunctionTag.renderPause,
_pause,
duration: Duration(seconds: 5),
);
}
resume() {
throttler.cancel(DebounceTag.renderPause);
throttler.cancel(FunctionTag.renderPause);
_resume();
}
@@ -54,4 +54,4 @@ class Render {
}
}
final render = system.isDesktop ? Render() : null;
final Render? render = system.isDesktop ? Render() : null;

View File

@@ -68,7 +68,7 @@ class Request {
final remoteVersion = data['tag_name'];
final version = globalState.packageInfo.version;
final hasUpdate =
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
if (!hasUpdate) return null;
return data;
}
@@ -130,7 +130,7 @@ class Request {
if (response.statusCode != HttpStatus.ok) {
return false;
}
return (response.data as String) == helperTag;
return (response.data as String) == globalState.coreSHA256;
} catch (_) {
return false;
}

View File

@@ -47,6 +47,10 @@ extension StringExtension on String {
return false;
}
}
// bool containsToLower(String target) {
// return toLowerCase().contains(target);
// }
}
extension StringExtensionSafe on String? {

View File

@@ -55,18 +55,24 @@ class System {
}
Future<AuthorizeCode> authorizeCore() async {
if (Platform.isAndroid) {
return AuthorizeCode.none;
}
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
final isAdmin = await checkIsAdmin();
if (isAdmin) {
return AuthorizeCode.none;
}
if (Platform.isWindows) {
final result = await windows?.registerService();
if (result == true) {
return AuthorizeCode.success;
}
return AuthorizeCode.error;
} else if (Platform.isMacOS) {
}
if (Platform.isMacOS) {
final shell = 'chown root:admin $corePath; chmod +sx $corePath';
final arguments = [
"-e",

View File

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

View File

@@ -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,
),
@@ -80,7 +80,7 @@ class Tray {
);
}
menuItems.add(MenuItem.separator());
if (!Platform.isWindows) {
if (Platform.isMacOS) {
for (final group in trayState.groups) {
List<MenuItem> subMenuItems = [];
for (final proxy in group.all) {

View File

@@ -5,9 +5,10 @@ import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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;
@@ -230,7 +231,64 @@ class Other {
}
int getProfilesColumns(double viewWidth) {
return max((viewWidth / 350).floor(), 1);
return max((viewWidth / 320).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() {
@@ -266,6 +324,15 @@ class Other {
}
return "";
}
SingleActivator controlSingleActivator(LogicalKeyboardKey trigger) {
final control = Platform.isMacOS ? false : true;
return SingleActivator(
trigger,
control: control,
meta: !control,
);
}
}
final other = Other();
final utils = Utils();

View File

@@ -8,6 +8,7 @@ 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/plugins/app.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
@@ -30,17 +31,18 @@ class AppController {
AppController(this.context, WidgetRef ref) : _ref = ref;
updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, () {
updateClashConfig(true);
debouncer.call(FunctionTag.updateClashConfig, () async {
final isPatch = globalState.appState.needApply ? false : true;
await updateClashConfig(isPatch);
});
}
updateGroupsDebounce() {
debouncer.call(DebounceTag.updateGroups, updateGroups);
debouncer.call(FunctionTag.updateGroups, updateGroups);
}
addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
debouncer.call(FunctionTag.addCheckIpNum, () {
_ref.read(checkIpNumProvider.notifier).add();
});
}
@@ -48,17 +50,17 @@ class AppController {
applyProfileDebounce({
bool silence = false,
}) {
debouncer.call(DebounceTag.applyProfile, (silence) {
debouncer.call(FunctionTag.applyProfile, (silence) {
applyProfile(silence: silence);
}, args: [silence]);
}
savePreferencesDebounce() {
debouncer.call(DebounceTag.savePreferences, savePreferences);
debouncer.call(FunctionTag.savePreferences, savePreferences);
}
changeProxyDebounce(String groupName, String proxyName) {
debouncer.call(DebounceTag.changeProxy,
debouncer.call(FunctionTag.changeProxy,
(String groupName, String proxyName) async {
await changeProxy(
groupName: groupName,
@@ -70,7 +72,7 @@ class AppController {
restartCore() async {
await clashService?.reStart();
await initCore();
await _initCore();
if (_ref.read(runTimeProvider.notifier).isStart) {
await globalState.handleStart();
@@ -100,7 +102,6 @@ class AppController {
_ref.read(trafficsProvider.notifier).clear();
_ref.read(totalTrafficProvider.notifier).value = Traffic();
_ref.read(runTimeProvider.notifier).value = null;
// tray.updateTrayTitle(null);
addCheckIpNumDebounce();
}
}
@@ -153,7 +154,7 @@ class AppController {
updateLocalIp() async {
_ref.read(localIpProvider.notifier).value = null;
await Future.delayed(commonDuration);
_ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress();
_ref.read(localIpProvider.notifier).value = await utils.getLocalIpAddress();
}
Future<void> updateProfile(Profile profile) async {
@@ -260,9 +261,7 @@ class AppController {
final patchConfig = _ref.read(patchClashConfigProvider);
final appSetting = _ref.read(appSettingProvider);
bool enableTun = patchConfig.tun.enable;
if (enableTun != lastTunEnable &&
lastTunEnable == false &&
!Platform.isAndroid) {
if (enableTun != lastTunEnable && lastTunEnable == false) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.none:
@@ -283,6 +282,9 @@ class AppController {
final res = await clashCore.updateConfig(
globalState.getUpdateConfigParams(isPatch),
);
if (isPatch == false) {
_ref.read(needApplyProvider.notifier).value = false;
}
if (res.isNotEmpty) throw res;
lastTunEnable = enableTun;
lastProfileModified = await profile?.profileLastModified;
@@ -311,6 +313,10 @@ class AppController {
handleChangeProfile() {
_ref.read(delayDataSourceProvider.notifier).value = {};
applyProfile();
_ref.read(logsProvider.notifier).value = FixedList(500);
_ref.read(requestsProvider.notifier).value = FixedList(500);
globalState.cacheHeightMap = {};
globalState.cacheScrollPosition = {};
}
updateBrightness(Brightness brightness) {
@@ -331,23 +337,22 @@ class AppController {
try {
await updateProfile(profile);
} catch (e) {
_ref.read(logsProvider.notifier).addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
commonPrint.log(e.toString());
}
}
}
Future<void> updateGroups() async {
_ref.read(groupsProvider.notifier).value = await retry(
task: () async {
return await clashCore.getProxiesGroups();
},
retryIf: (res) => res.isEmpty,
);
try {
_ref.read(groupsProvider.notifier).value = await retry(
task: () async {
return await clashCore.getProxiesGroups();
},
retryIf: (res) => res.isEmpty,
);
} catch (_) {
_ref.read(groupsProvider.notifier).value = [];
}
}
updateProfiles() async {
@@ -359,10 +364,6 @@ class AppController {
}
}
updateSystemColorSchemes(ColorSchemes colorSchemes) {
_ref.read(appSchemesProvider.notifier).value = colorSchemes;
}
savePreferences() async {
commonPrint.log("save preferences");
await preferences.saveConfig(globalState.config);
@@ -385,6 +386,9 @@ class AppController {
}
handleBackOrExit() async {
if (_ref.read(backBlockProvider)) {
return;
}
if (_ref.read(appSettingProvider).minimizeOnExit) {
if (system.isDesktop) {
await savePreferencesDebounce();
@@ -395,18 +399,34 @@ class AppController {
}
}
backBlock() {
_ref.read(backBlockProvider.notifier).value = true;
}
unBackBlock() {
_ref.read(backBlockProvider.notifier).value = false;
}
handleExit() async {
try {
await updateStatus(false);
await proxy?.stopProxy();
await clashCore.shutdown();
await clashService?.destroy();
await proxy?.stopProxy();
await savePreferences();
} finally {
system.exit();
}
}
Future handleClear() async {
await preferences.clearPreferences();
commonPrint.log("clear preferences");
globalState.config = Config(
themeProps: defaultThemeProps,
);
}
autoCheckUpdate() async {
if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
final res = await request.checkForUpdate();
@@ -417,13 +437,13 @@ class AppController {
Map<String, dynamic>? data,
bool handleError = false,
}) async {
if(globalState.isPre){
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,
@@ -478,21 +498,22 @@ class AppController {
await handleExit();
}
Future<void> initCore() async {
Future<void> _initCore() async {
final isInit = await clashCore.isInit;
if (!isInit) {
await clashCore.init();
await clashCore.setState(
globalState.getCoreState(),
);
await clashCore.init();
}
await applyProfile();
}
init() async {
await _handlePreference();
await _handlerDisclaimer();
await initCore();
FlutterError.onError = (details) {
commonPrint.log(details.stack.toString());
};
await _initCore();
await _initStatus();
updateTray(true);
autoLaunch?.updateStatus(
@@ -505,6 +526,8 @@ class AppController {
} else {
window?.hide();
}
await _handlePreference();
await _handlerDisclaimer();
_ref.read(initProvider.notifier).value = true;
}
@@ -668,9 +691,9 @@ class AppController {
List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
(a, b) => other.sortByChar(
other.getPinyin(a.name),
other.getPinyin(b.name),
(a, b) => utils.sortByChar(
utils.getPinyin(a.name),
utils.getPinyin(b.name),
),
);
}
@@ -682,10 +705,16 @@ class AppController {
return List.of(proxies)
..sort(
(a, b) {
final aDelay =
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
final bDelay =
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
final aDelay = _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;
}
@@ -746,6 +775,17 @@ class AppController {
);
}
Future<List<Package>> getPackages() async {
if (_ref.read(isMobileViewProvider)) {
await Future.delayed(commonDuration);
}
if (_ref.read(packagesProvider).isEmpty) {
_ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
}
return _ref.read(packagesProvider);
}
updateStart() {
updateStatus(!_ref.read(runTimeProvider.notifier).isStart);
}
@@ -860,7 +900,7 @@ class AppController {
return utf8.encode(logsRawString);
});
return await picker.saveFile(
other.logFile,
utils.logFile,
Uint8List.fromList(data),
) !=
null;
@@ -934,30 +974,39 @@ class AppController {
}
_recovery(Config config, RecoveryOption recoveryOption) {
final recoveryStrategy = _ref.read(appSettingProvider.select(
(state) => state.recoveryStrategy,
));
final profiles = config.profiles;
for (final profile in profiles) {
_ref.read(profilesProvider.notifier).setProfile(profile);
if (recoveryStrategy == RecoveryStrategy.override) {
_ref.read(profilesProvider.notifier).value = profiles;
} else {
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;
if (!onlyProfiles) {
_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;
}
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile == null) {
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
}
_ref.read(patchClashConfigProvider.notifier).value =
config.patchClashConfig;
_ref.read(appSettingProvider.notifier).value = config.appSetting;
_ref.read(currentProfileIdProvider.notifier).value =
config.currentProfileId;
_ref.read(appDAVSettingProvider.notifier).value = config.dav;
_ref.read(themeSettingProvider.notifier).value = config.themeProps;
_ref.read(windowSettingProvider.notifier).value = config.windowProps;
_ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
_ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle;
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
}
}

View File

@@ -91,7 +91,14 @@ enum Mode { rule, global, direct }
enum ViewMode { mobile, laptop, desktop }
enum LogLevel { debug, info, warning, error, silent }
enum LogLevel {
debug,
info,
warning,
error,
silent,
app,
}
enum TransportProtocol { udp, tcp }
@@ -262,6 +269,7 @@ enum ActionMethod {
getCountryCode,
getMemory,
getProfile,
crash,
///Android,
setFdMap,
@@ -283,8 +291,9 @@ enum WindowsHelperServiceStatus {
running,
}
enum DebounceTag {
enum FunctionTag {
updateClashConfig,
updateStatus,
updateGroups,
addCheckIpNum,
applyProfile,
@@ -299,6 +308,9 @@ enum DebounceTag {
updatePageIndex,
pageChange,
proxiesTabChange,
logs,
requests,
}
enum DashboardWidget {
@@ -308,6 +320,12 @@ enum DashboardWidget {
child: NetworkSpeed(),
),
),
outboundModeV2(
GridItem(
crossAxisCellCount: 8,
child: OutboundModeV2(),
),
),
outboundMode(
GridItem(
crossAxisCellCount: 4,
@@ -333,6 +351,15 @@ enum DashboardWidget {
),
platforms: desktopPlatforms,
),
vpnButton(
GridItem(
crossAxisCellCount: 4,
child: VpnButton(),
),
platforms: [
SupportPlatform.Android,
],
),
systemProxyButton(
GridItem(
crossAxisCellCount: 4,
@@ -447,3 +474,14 @@ enum RuleTarget {
DIRECT,
REJECT,
}
enum RecoveryStrategy {
compatible,
override,
}
enum CacheTag {
logs,
rules,
requests,
}

View File

@@ -1,7 +1,11 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@immutable
class Contributor {
@@ -43,6 +47,15 @@ class AboutFragment extends StatelessWidget {
_checkUpdate(context);
},
),
ListItem(
title: Text(appLocalizations.contactMe),
onTap: () {
globalState.showMessage(
title: appLocalizations.contactMe,
message: TextSpan(text: "chen08209@gmail.com"),
);
},
),
ListItem(
title: const Text("Telegram"),
onTap: () {
@@ -116,33 +129,43 @@ class AboutFragment extends StatelessWidget {
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Image.asset(
'assets/images/icon.png',
width: 64,
height: 64,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
Consumer(builder: (_, ref, ___) {
return _DeveloperModeDetector(
child: Wrap(
spacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
appName,
style: Theme.of(context).textTheme.headlineSmall,
Padding(
padding: const EdgeInsets.all(12),
child: Image.asset(
'assets/images/icon.png',
width: 64,
height: 64,
),
),
Text(
globalState.packageInfo.version,
style: Theme.of(context).textTheme.labelLarge,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appName,
style: Theme.of(context).textTheme.headlineSmall,
),
Text(
globalState.packageInfo.version,
style: Theme.of(context).textTheme.labelLarge,
)
],
)
],
)
],
),
),
onEnterDeveloperMode: () {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(developerMode: true),
);
context.showNotifier(appLocalizations.developerModeEnableTip);
},
);
}),
const SizedBox(
height: 24,
),
@@ -179,10 +202,7 @@ class Avatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
return GestureDetector(
child: Column(
children: [
SizedBox(
@@ -209,3 +229,52 @@ class Avatar extends StatelessWidget {
);
}
}
class _DeveloperModeDetector extends StatefulWidget {
final Widget child;
final VoidCallback onEnterDeveloperMode;
const _DeveloperModeDetector({
required this.child,
required this.onEnterDeveloperMode,
});
@override
State<_DeveloperModeDetector> createState() => _DeveloperModeDetectorState();
}
class _DeveloperModeDetectorState extends State<_DeveloperModeDetector> {
int _counter = 0;
Timer? _timer;
void _handleTap() {
_counter++;
if (_counter >= 5) {
widget.onEnterDeveloperMode();
_resetCounter();
} else {
_timer?.cancel();
_timer = Timer(Duration(seconds: 1), _resetCounter);
}
}
void _resetCounter() {
_counter = 0;
_timer?.cancel();
_timer = null;
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: widget.child,
);
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/enum/enum.dart';
@@ -22,21 +23,14 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
List<String> acceptList = [];
List<String> rejectList = [];
late ScrollController _controller;
final _completer = Completer();
@override
void initState() {
super.initState();
_updateInitList();
_controller = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appState;
if (appState.packages.isEmpty) {
Future.delayed(const Duration(milliseconds: 300), () async {
ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
});
}
});
_completer.complete(globalState.appController.getPackages());
}
@override
@@ -113,15 +107,11 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
}
_intelligentSelected() async {
final appState = globalState.appState;
final config = globalState.config;
final accessControl = config.vpnProps.accessControl;
final packageNames = appState.packages
.where(
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
final packageNames = ref.read(
packageListSelectorStateProvider.select(
(state) => state.list.map((item) => item.packageName),
),
);
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
@@ -194,7 +184,7 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
final state = ref.watch(packageListSelectorStateProvider);
final accessControl = state.accessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
final packages = state.getSortList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
@@ -323,31 +313,42 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
),
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);
},
child: FutureBuilder(
future: _completer.future,
builder: (_, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return Center(
child: CircularProgressIndicator(),
);
}
return packages.isEmpty
? NullStatus(
label: appLocalizations.noData,
)
: 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);
},
);
},
),
);
},
),
),
),
}),
)
],
),
),
@@ -482,14 +483,20 @@ class AccessControlSearchDelegate extends SearchDelegate {
final lowQuery = query.toLowerCase();
return Consumer(
builder: (context, ref, __) {
final state = ref.watch(packageListSelectorStateProvider);
final accessControl = state.accessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
final vm3 = ref.watch(
packageListSelectorStateProvider.select(
(state) => VM3(
a: state.getSortList(
state.accessControl.mode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
),
b: state.accessControl.enable,
c: state.accessControl.currentList,
),
),
);
final packages = vm3.a;
final queryPackages = packages
.where(
(package) =>
@@ -497,8 +504,8 @@ class AccessControlSearchDelegate extends SearchDelegate {
package.packageName.contains(lowQuery),
)
.toList();
final isAccessControl = state.accessControl.enable;
final currentList = accessControl.currentList;
final isAccessControl = vm3.b;
final currentList = vm3.c;
final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
return DisabledMask(
@@ -579,13 +586,6 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
};
}
String _getTextWithIsFilterSystemApp(bool isFilterSystemApp) {
return switch (isFilterSystemApp) {
true => appLocalizations.onlyOtherApps,
false => appLocalizations.allApps,
};
}
List<Widget> _buildModeSetting() {
return generateSection(
title: appLocalizations.mode,
@@ -673,25 +673,39 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
scrollDirection: Axis.horizontal,
child: Consumer(
builder: (_, ref, __) {
final isFilterSystemApp = ref.watch(
vpnSettingProvider
.select((state) => state.accessControl.isFilterSystemApp),
final vm2 = ref.watch(
vpnSettingProvider.select(
(state) => VM2(
a: state.accessControl.isFilterSystemApp,
b: state.accessControl.isFilterNonInternetApp,
),
),
);
return Wrap(
spacing: 16,
children: [
for (final item in [false, true])
SettingTextCard(
_getTextWithIsFilterSystemApp(item),
isSelected: isFilterSystemApp == item,
onPressed: () {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
isFilterSystemApp: item,
),
);
},
)
SettingTextCard(
appLocalizations.systemApp,
isSelected: vm2.a == false,
onPressed: () {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
isFilterSystemApp: !vm2.a,
),
);
},
),
SettingTextCard(
appLocalizations.noNetworkApp,
isSelected: vm2.b == false,
onPressed: () {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
isFilterNonInternetApp: !vm2.b,
),
);
},
)
],
);
},

View File

@@ -8,10 +8,12 @@ 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/input.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
class BackupAndRecovery extends ConsumerWidget {
const BackupAndRecovery({super.key});
@@ -75,7 +77,7 @@ class BackupAndRecovery extends ConsumerWidget {
() async {
final backupData = await globalState.appController.backupData();
final value = await picker.saveFile(
other.getBackupFileName(),
utils.getBackupFileName(),
Uint8List.fromList(backupData),
);
if (value == null) return false;
@@ -134,6 +136,30 @@ class BackupAndRecovery extends ConsumerWidget {
);
}
_handleUpdateRecoveryStrategy(WidgetRef ref) async {
final recoveryStrategy = ref.read(appSettingProvider.select(
(state) => state.recoveryStrategy,
));
final res = await globalState.showCommonDialog(
child: OptionsDialog<RecoveryStrategy>(
title: appLocalizations.recoveryStrategy,
options: RecoveryStrategy.values,
textBuilder: (mode) => Intl.message(
"recoveryStrategy_${mode.name}",
),
value: recoveryStrategy,
),
);
if (res == null) {
return;
}
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
recoveryStrategy: res,
),
);
}
@override
Widget build(BuildContext context, ref) {
final dav = ref.watch(appDAVSettingProvider);
@@ -176,25 +202,25 @@ class BackupAndRecovery extends ConsumerWidget {
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,
),
child:
snapshot.connectionState != ConnectionState.done
? 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,
),
),
);
},
@@ -256,6 +282,26 @@ class BackupAndRecovery extends ConsumerWidget {
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
ListHeader(title: appLocalizations.options),
Consumer(builder: (_, ref, __) {
final recoveryStrategy = ref.watch(appSettingProvider.select(
(state) => state.recoveryStrategy,
));
return ListItem(
onTap: () {
_handleUpdateRecoveryStrategy(ref);
},
title: Text(appLocalizations.recoveryStrategy),
trailing: FilledButton(
onPressed: () {
_handleUpdateRecoveryStrategy(ref);
},
child: Text(
Intl.message("recoveryStrategy_${recoveryStrategy.name}"),
),
),
);
}),
],
);
}

View File

@@ -61,7 +61,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
if (res != true) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
dns: defaultDns,

View File

@@ -301,8 +301,11 @@ class RouteAddressItem extends ConsumerWidget {
title: appLocalizations.routeAddress,
widget: Consumer(
builder: (_, ref, __) {
final routeAddress = ref.watch(patchClashConfigProvider
.select((state) => state.tun.routeAddress));
final routeAddress = ref.watch(
patchClashConfigProvider.select(
(state) => state.tun.routeAddress,
),
);
return ListInputPage(
title: appLocalizations.routeAddress,
items: routeAddress,
@@ -371,7 +374,9 @@ class NetworkListView extends ConsumerWidget {
return;
}
ref.read(vpnSettingProvider.notifier).updateState(
(state) => defaultVpnProps,
(state) => defaultVpnProps.copyWith(
accessControl: state.accessControl,
),
);
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(

View File

@@ -11,8 +11,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart';
double _preOffset = 0;
class RequestsFragment extends ConsumerStatefulWidget {
const RequestsFragment({super.key});
@@ -22,14 +20,13 @@ class RequestsFragment extends ConsumerStatefulWidget {
class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin {
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = [];
final ScrollController _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
final _requestsStateNotifier = ValueNotifier<ConnectionsState>(
const ConnectionsState(loading: true),
);
List<Connection> _requests = [];
final _tag = CacheTag.requests;
late ScrollController _scrollController;
bool _isLoad = false;
double _currentMaxWidth = 0;
@@ -49,10 +46,14 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
@override
void initState() {
super.initState();
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: globalState.appState.requests.list,
final preOffset = globalState.cacheScrollPosition[_tag] ?? -1;
_scrollController = ScrollController(
initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite,
);
_requests = globalState.appState.requests.list;
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: _requests,
);
ref.listenManual(
isCurrentPageProvider(
PageLabel.requests,
@@ -74,7 +75,6 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
updateRequestsThrottler();
}
},
fireImmediately: true,
);
}
@@ -99,14 +99,7 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
final lines = (chainSize.height / baseHeight).round();
final computerHeight =
size.height + chainSize.height + 24 + 24 * (lines - 1);
return computerHeight;
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_key.currentState?.clearCache();
}
return computerHeight + 8 + 32 + globalState.measure.bodyMediumHeight;
}
@override
@@ -118,7 +111,7 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
}
updateRequestsThrottler() {
throttler.call("request", () {
throttler.call(FunctionTag.requests, () {
final isEquality = connectionListEquality.equals(
_requests,
_requestsStateNotifier.value.connections,
@@ -127,13 +120,51 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: _requests,
);
if(mounted){
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: _requests,
);
}
});
}, duration: commonDuration);
}
_preLoad() {
if (_isLoad == true) {
return;
}
_isLoad = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) {
return;
}
final isMobileView = ref.read(isMobileViewProvider);
if (isMobileView) {
await Future.delayed(Duration(milliseconds: 300));
}
final parts = _requests.batch(10);
globalState.cacheHeightMap[_tag] ??= FixedMap(
_requests.length,
);
for (int i = 0; i < parts.length; i++) {
final part = parts[i];
await Future(
() {
for (final request in part) {
globalState.cacheHeightMap[_tag]?.updateCacheValue(
request.id,
() => _calcCacheHeight(request),
);
}
},
);
}
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
loading: false,
);
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
@@ -147,76 +178,86 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
Platform.isAndroid,
),
);
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
_currentMaxWidth = 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,
child: TextScaleNotification(
child: ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier,
builder: (_, state, __) {
_preLoad();
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();
final content = connections.isEmpty
? NullStatus(
label: appLocalizations.nullRequestsDesc,
)
: Align(
alignment: Alignment.topCenter,
child: ScrollToEndBox(
controller: _scrollController,
tag: _tag,
dataSource: connections,
child: CommonScrollBar(
controller: _scrollController,
child: CacheItemExtentListView(
tag: _tag,
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index) {
if (index.isOdd) {
return 0;
}
return _calcCacheHeight(
connections[index ~/ 2]);
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
keyBuilder: (int index) {
if (index.isOdd) {
return "divider";
}
return connections[index ~/ 2].id;
},
),
),
),
);
return FadeBox(
child: state.loading
? Center(
child: CircularProgressIndicator(),
)
: content,
);
}
final items = connections
.map<Widget>(
(connection) => ConnectionItem(
key: Key(connection.id),
connection: connection,
onClickKeyword: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: CacheItemExtentListView(
key: _key,
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
keyBuilder: (int index) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return "divider";
}
final connection = connections[(index / 2).floor()];
return connection.id;
},
),
),
),
);
},
),
onNotification: (_) {
globalState.cacheHeightMap[_tag]?.clear();
},
),
);

View File

@@ -6,6 +6,7 @@ import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'widgets/start_button.dart';
class DashboardFragment extends ConsumerStatefulWidget {
@@ -66,7 +67,16 @@ class _DashboardFragmentState extends ConsumerState<DashboardFragment>
valueListenable: key.currentState!.isEditNotifier,
builder: (_, isEdit, ___) {
return isEdit
? Icon(Icons.save)
? SystemBackBlock(
child: CommonPopScope(
child: Icon(Icons.save),
onPop: () {
key.currentState!.isEditNotifier.value =
!key.currentState!.isEditNotifier.value;
return false;
},
),
)
: Icon(
Icons.edit,
);
@@ -93,7 +103,7 @@ class _DashboardFragmentState extends ConsumerState<DashboardFragment>
@override
Widget build(BuildContext context) {
final dashboardState = ref.watch(dashboardStateProvider);
final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
final columns = max(4 * ((dashboardState.viewWidth / 320).ceil()), 8);
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
@@ -103,8 +113,8 @@ class _DashboardFragmentState extends ConsumerState<DashboardFragment>
child: SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
crossAxisSpacing: 16.ap,
mainAxisSpacing: 16.ap,
children: [
...dashboardState.dashboardWidgets
.where(

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
@@ -57,37 +58,43 @@ class _MemoryInfoState extends State<MemoryInfo> {
onPressed: () {
clashCore.requestGc();
},
child: Column(
children: [
ValueListenableBuilder(
valueListenable: _memoryInfoStateNotifier,
builder: (_, trafficValue, __) {
return Padding(
padding: baseInfoEdgeInsets.copyWith(
bottom: 0,
top: 12,
),
child: Row(
children: [
Text(
trafficValue.showValue,
style:
context.textTheme.bodyMedium?.toLight.adjustSize(1),
),
SizedBox(
width: 8,
),
Text(
trafficValue.showUnit,
style:
context.textTheme.bodyMedium?.toLight.adjustSize(1),
)
],
),
);
},
),
],
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: ValueListenableBuilder(
valueListenable: _memoryInfoStateNotifier,
builder: (_, trafficValue, __) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
trafficValue.showValue,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
),
SizedBox(
width: 8,
),
Text(
trafficValue.showUnit,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
)
],
);
},
),
)
],
),
),
),
);

View File

@@ -1,23 +1,11 @@
import 'dart:async';
import 'package:dio/dio.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/app.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';
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: false,
isLoading: true,
ipInfo: null,
),
);
class NetworkDetection extends ConsumerStatefulWidget {
const NetworkDetection({super.key});
@@ -26,101 +14,6 @@ class NetworkDetection extends ConsumerStatefulWidget {
}
class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
bool? _preIsStart;
Timer? _setTimeoutTimer;
CancelToken? cancelToken;
@override
void initState() {
ref.listenManual(checkIpNumProvider, (prev, next) {
if (prev != next) {
_startCheck();
}
});
if (!_networkDetectionState.value.isTesting &&
_networkDetectionState.value.isLoading) {
_startCheck();
}
super.initState();
}
_startCheck() async {
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
debouncer.call(
DebounceTag.checkIp,
_checkIp,
);
}
_checkIp() async {
final appState = globalState.appState;
final isInit = appState.isInit;
if (!isInit) return;
final isStart = appState.runTime != null;
if (_preIsStart == false &&
_preIsStart == isStart &&
_networkDetectionState.value.ipInfo != null) {
return;
}
_clearSetTimeoutTimer();
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isLoading: true,
ipInfo: null,
);
_preIsStart = isStart;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
cancelToken = CancelToken();
try {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true,
);
final ipInfo = await request.checkIp(cancelToken: cancelToken);
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false,
);
if (ipInfo != null) {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isLoading: false,
ipInfo: ipInfo,
);
return;
}
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isLoading: false,
ipInfo: null,
);
});
} catch (e) {
if (e.toString() == "cancelled") {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isLoading: true,
ipInfo: null,
);
}
}
}
@override
void dispose() {
_clearSetTimeoutTimer();
super.dispose();
}
_clearSetTimeoutTimer() {
if (_setTimeoutTimer != null) {
_setTimeoutTimer?.cancel();
_setTimeoutTimer = null;
}
}
_countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase();
if (code.length != 2) {
@@ -136,7 +29,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
return SizedBox(
height: getWidgetHeight(1),
child: ValueListenableBuilder<NetworkDetectionState>(
valueListenable: _networkDetectionState,
valueListenable: detectionState.state,
builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isLoading = state.isLoading;
@@ -206,7 +99,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
);
},
icon: Icon(
size: 16,
size: 16.ap,
Icons.info_outline,
color: context.colorScheme.onSurfaceVariant,
),

View File

@@ -17,58 +17,146 @@ class OutboundMode extends StatelessWidget {
height: height,
child: Consumer(
builder: (_, ref, __) {
final mode =
ref.watch(patchClashConfigProvider.select((state) => state.mode));
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(
top: 12,
bottom: 16,
),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (final item in Mode.values)
Flexible(
child: ListItem.radio(
dense: true,
horizontalTitleGap: 4,
padding: const EdgeInsets.only(
left: 12,
right: 16,
),
delegate: RadioDelegate(
value: item,
groupValue: mode,
onChanged: (value) async {
if (value == null) {
return;
}
globalState.appController.changeMode(value);
},
),
title: Text(
Intl.message(item.name),
style: Theme.of(context)
.textTheme
.bodyMedium
?.toSoftBold,
),
),
),
],
),
final mode = ref.watch(
patchClashConfigProvider.select(
(state) => state.mode,
),
);
return Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent),
child: CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(
top: 12,
bottom: 16,
),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (final item in Mode.values)
Flexible(
fit: FlexFit.tight,
child: ListItem.radio(
dense: true,
horizontalTitleGap: 4,
padding: EdgeInsets.only(
left: 12.ap,
right: 16.ap,
),
delegate: RadioDelegate(
value: item,
groupValue: mode,
onChanged: (value) async {
if (value == null) {
return;
}
globalState.appController.changeMode(value);
},
),
title: Text(
Intl.message(item.name),
style: Theme.of(context)
.textTheme
.bodyMedium
?.toSoftBold,
),
),
),
],
),
),
));
},
),
);
}
}
class OutboundModeV2 extends StatelessWidget {
const OutboundModeV2({super.key});
Color _getTextColor(BuildContext context, Mode mode) {
return switch (mode) {
Mode.rule => context.colorScheme.onSecondaryContainer,
Mode.global => context.colorScheme.onPrimaryContainer,
Mode.direct => context.colorScheme.onTertiaryContainer,
};
}
@override
Widget build(BuildContext context) {
final height = getWidgetHeight(0.72);
return SizedBox(
height: height,
child: CommonCard(
padding: EdgeInsets.zero,
child: Consumer(
builder: (_, ref, __) {
final mode = ref.watch(
patchClashConfigProvider.select(
(state) => state.mode,
),
);
final thumbColor = switch (mode) {
Mode.rule => context.colorScheme.secondaryContainer,
Mode.global => globalState.theme.darken3PrimaryContainer,
Mode.direct => context.colorScheme.tertiaryContainer,
};
return Container(
constraints: BoxConstraints.expand(),
child: CommonTabBar<Mode>(
children: Map.fromEntries(
Mode.values.map(
(item) => MapEntry(
item,
Container(
clipBehavior: Clip.antiAlias,
alignment: Alignment.center,
decoration: BoxDecoration(),
height: height - 16,
child: Text(
Intl.message(item.name),
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(1)
.copyWith(
color: item == mode
? _getTextColor(
context,
item,
)
: null,
),
),
),
),
),
),
padding: EdgeInsets.symmetric(horizontal: 8),
groupValue: mode,
onValueChanged: (value) {
if (value == null) {
return;
}
globalState.appController.changeMode(value);
},
thumbColor: thumbColor,
),
);
},
),
),
);
}
}

View File

@@ -165,3 +165,87 @@ class SystemProxyButton extends StatelessWidget {
);
}
}
class VpnButton extends StatelessWidget {
const VpnButton({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
onPressed: () {
showSheet(
context: context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: generateListView(
generateSection(
items: [
const VPNItem(),
const VpnSystemProxyItem(),
const TunStackItem(),
],
),
),
title: "VPN",
);
},
);
},
info: Info(
label: "VPN",
iconData: Icons.stacked_line_chart,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
),
),
Consumer(
builder: (_, ref, __) {
final enable = ref.watch(
vpnSettingProvider.select(
(state) => state.enable,
),
);
return Switch(
value: enable,
onChanged: (value) {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
enable: value,
),
);
},
);
},
)
],
),
),
),
);
}
}

View File

@@ -1,5 +1,5 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
@@ -35,11 +35,15 @@ class _StartButtonState extends State<StartButton>
}
handleSwitchStart() {
if (isStart == globalState.appState.isStart) {
isStart = !isStart;
updateController();
globalState.appController.updateStatus(isStart);
}
isStart = !isStart;
updateController();
debouncer.call(
FunctionTag.updateStatus,
() {
globalState.appController.updateStatus(isStart);
},
duration: moreDuration,
);
}
updateController() {
@@ -71,10 +75,10 @@ class _StartButtonState extends State<StartButton>
final textWidth = globalState.measure
.computeTextSize(
Text(
other.getTimeDifference(
utils.getTimeDifference(
DateTime.now(),
),
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
style: context.textTheme.titleMedium?.toSoftBold,
),
)
.width +
@@ -123,10 +127,14 @@ class _StartButtonState extends State<StartButton>
child: Consumer(
builder: (_, ref, __) {
final runTime = ref.watch(runTimeProvider);
final text = other.getTimeText(runTime);
final text = utils.getTimeText(runTime);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
style: Theme.of(context)
.textTheme
.titleMedium
?.toSoftBold
.copyWith(color: context.colorScheme.onPrimaryContainer),
);
},
),

View File

@@ -0,0 +1,118 @@
import 'package:fl_clash/clash/core.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/common.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 '../providers/app.dart';
class DeveloperView extends ConsumerWidget {
const DeveloperView({super.key});
Widget _getDeveloperList(BuildContext context, WidgetRef ref) {
return generateSectionV2(
title: appLocalizations.options,
items: [
ListItem(
title: Text(appLocalizations.messageTest),
onTap: () {
context.showNotifier(
appLocalizations.messageTestTip,
);
},
),
ListItem(
title: Text(appLocalizations.logsTest),
onTap: () {
for (int i = 0; i < 1000; i++) {
ref.read(requestsProvider.notifier).addRequest(Connection(
id: utils.id,
start: DateTime.now(),
metadata: Metadata(
uid: i * i,
network: utils.generateRandomString(
maxLength: 1000,
minLength: 20,
),
sourceIP: '',
sourcePort: '',
destinationIP: '',
destinationPort: '',
host: '',
process: '',
remoteDestination: "",
),
chains: ["chains"],
));
globalState.appController.addLog(
Log.app(
utils.generateRandomString(
maxLength: 200,
minLength: 20,
),
),
);
}
},
),
ListItem(
title: Text(appLocalizations.crashTest),
onTap: () {
clashCore.clashInterface.crash();
},
),
ListItem(
title: Text(appLocalizations.clearData),
onTap: () async {
await globalState.appController.handleClear();
},
)
],
);
}
@override
Widget build(BuildContext context, ref) {
final enable = ref.watch(
appSettingProvider.select(
(state) => state.developerMode,
),
);
return SingleChildScrollView(
padding: baseInfoEdgeInsets,
child: Column(
children: [
CommonCard(
type: CommonCardType.filled,
radius: 18,
child: ListItem.switchItem(
padding: const EdgeInsets.only(
left: 16,
right: 16,
),
title: Text(appLocalizations.developerMode),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
developerMode: value,
),
);
},
),
),
),
SizedBox(
height: 16,
),
_getDeveloperList(context, ref)
],
),
);
}
}

View File

@@ -11,3 +11,4 @@ export 'backup_and_recovery.dart';
export 'resources.dart';
export 'connection/requests.dart';
export 'connection/connections.dart';
export 'developer.dart';

View File

@@ -103,7 +103,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
modifiers: modifiers,
key: key.usbHidUsage,
);
return true;
return false;
}
@override
@@ -157,59 +157,65 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
@override
Widget build(BuildContext context) {
return CommonDialog(
title: IntlExt.actionMessage(widget.hotKeyAction.action.name),
actions: [
TextButton(
onPressed: () {
_handleRemove();
},
child: Text(appLocalizations.remove),
),
const SizedBox(
width: 8,
),
TextButton(
onPressed: () {
_handleConfirm();
},
child: Text(
appLocalizations.confirm,
return Focus(
onKeyEvent: (_, __) {
return KeyEventResult.handled;
},
autofocus: true,
child: CommonDialog(
title: IntlExt.actionMessage(widget.hotKeyAction.action.name),
actions: [
TextButton(
onPressed: () {
_handleRemove();
},
child: Text(appLocalizations.remove),
),
),
],
child: ValueListenableBuilder(
valueListenable: hotKeyActionNotifier,
builder: (_, hotKeyAction, ___) {
final key = hotKeyAction.key;
final modifiers = hotKeyAction.modifiers;
return SizedBox(
width: dialogCommonWidth,
child: key != null
? Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final modifier in modifiers)
const SizedBox(
width: 8,
),
TextButton(
onPressed: () {
_handleConfirm();
},
child: Text(
appLocalizations.confirm,
),
),
],
child: ValueListenableBuilder(
valueListenable: hotKeyActionNotifier,
builder: (_, hotKeyAction, ___) {
final key = hotKeyAction.key;
final modifiers = hotKeyAction.modifiers;
return SizedBox(
width: dialogCommonWidth,
child: key != null
? Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final modifier in modifiers)
KeyboardKeyBox(
keyboardKey: modifier.physicalKeys.first,
),
if (modifiers.isNotEmpty)
Text(
"+",
style: context.textTheme.titleMedium,
),
KeyboardKeyBox(
keyboardKey: modifier.physicalKeys.first,
keyboardKey: PhysicalKeyboardKey(key),
),
if (modifiers.isNotEmpty)
Text(
"+",
style: context.textTheme.titleMedium,
),
KeyboardKeyBox(
keyboardKey: PhysicalKeyboardKey(key),
),
],
)
: Text(
appLocalizations.pressKeyboard,
style: context.textTheme.titleMedium,
),
);
},
],
)
: Text(
appLocalizations.pressKeyboard,
style: context.textTheme.titleMedium,
),
);
},
),
),
);
}

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
@@ -8,8 +10,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/models.dart';
import '../widgets/widgets.dart';
double _preOffset = 0;
class LogsFragment extends ConsumerStatefulWidget {
const LogsFragment({super.key});
@@ -18,33 +18,27 @@ class LogsFragment extends ConsumerStatefulWidget {
}
class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
final _logsStateNotifier = ValueNotifier<LogsState>(
LogsState(loading: true),
);
late ScrollController _scrollController;
double _currentMaxWidth = 0;
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
final _tag = CacheTag.rules;
bool _isLoad = false;
List<Log> _logs = [];
@override
void initState() {
super.initState();
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: globalState.appState.logs.list,
final position = globalState.cacheScrollPosition[_tag] ?? -1;
_scrollController = ScrollController(
initialScrollOffset: position > 0 ? position : double.maxFinite,
);
ref.listenManual(
logsProvider.select((state) => state.list),
(prev, next) {
if (prev != next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
}
},
fireImmediately: true,
_logs = globalState.appState.logs.list;
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: _logs,
);
ref.listenManual(
isCurrentPageProvider(
@@ -59,6 +53,18 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
},
fireImmediately: true,
);
ref.listenManual(
logsProvider.select((state) => state.list),
(prev, next) {
if (prev != next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
}
},
);
}
@override
@@ -93,13 +99,6 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
super.dispose();
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_key.currentState?.clearCache();
}
}
_handleExport() async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
@@ -122,17 +121,17 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final height = globalState.measure
.computeTextSize(
Text(
log.payload ?? "",
style: globalState.appController.context.textTheme.bodyLarge,
log.payload,
style: context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
)
.height;
return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
return height + bodySmallHeight + 8 + bodyMediumHeight + 40 + 8;
}
updateLogsThrottler() {
throttler.call("logs", () {
throttler.call(FunctionTag.logs, () {
final isEquality = logListEquality.equals(
_logs,
_logsStateNotifier.value.logs,
@@ -141,83 +140,123 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: _logs,
);
if (mounted) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: _logs,
);
}
});
}, duration: commonDuration);
}
_preLoad() {
if (_isLoad == true) {
return;
}
_isLoad = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) {
return;
}
final isMobileView = ref.read(isMobileViewProvider);
if (isMobileView) {
await Future.delayed(Duration(milliseconds: 300));
}
final parts = _logs.batch(10);
globalState.cacheHeightMap[_tag] ??= FixedMap(
_logs.length,
);
for (int i = 0; i < parts.length; i++) {
final part = parts[i];
await Future(
() {
for (final log in part) {
globalState.cacheHeightMap[_tag]?.updateCacheValue(
log.payload,
() => _getItemHeight(log),
);
}
},
);
}
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
loading: false,
);
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 40);
return Align(
alignment: Alignment.topCenter,
child: ValueListenableBuilder<LogsState>(
valueListenable: _logsStateNotifier,
builder: (_, state, __) {
final logs = state.list;
if (logs.isEmpty) {
return NullStatus(
label: appLocalizations.nullLogsDesc,
);
}
final items = logs
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: CacheItemExtentListView(
key: _key,
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemBuilder: (_, index) {
return items[index];
},
itemExtentBuilder: (index) {
final item = items[index];
if (item.runtimeType == Divider) {
return 0;
}
final log = logs[(index / 2).floor()];
return _getItemHeight(log);
},
itemCount: items.length,
keyBuilder: (int index) {
final item = items[index];
if (item.runtimeType == Divider) {
return "divider";
}
final log = logs[(index / 2).floor()];
return log.payload ?? "";
_currentMaxWidth = constraints.maxWidth - 40;
return ValueListenableBuilder<LogsState>(
valueListenable: _logsStateNotifier,
builder: (_, state, __) {
_preLoad();
final logs = state.list;
final items = logs
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime),
log: log,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
),
);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
final content = logs.isEmpty
? NullStatus(
label: appLocalizations.nullLogsDesc,
)
: Align(
alignment: Alignment.topCenter,
child: CommonScrollBar(
controller: _scrollController,
child: ScrollToEndBox(
controller: _scrollController,
tag: _tag,
dataSource: logs,
child: CacheItemExtentListView(
tag: _tag,
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemBuilder: (_, index) {
return items[index];
},
itemExtentBuilder: (index) {
if (index.isOdd) {
return 0;
}
return _getItemHeight(logs[index ~/ 2]);
},
itemCount: items.length,
keyBuilder: (int index) {
if (index.isOdd) {
return "divider";
}
return logs[index ~/ 2].payload;
},
),
),
),
);
return FadeBox(
child: state.loading
? Center(
child: CircularProgressIndicator(),
)
: content,
);
},
);
},
);
@@ -242,14 +281,14 @@ class LogItem extends StatelessWidget {
vertical: 4,
),
title: SelectableText(
log.payload ?? '',
log.payload,
style: context.textTheme.bodyLarge,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
"${log.dateTime}",
log.dateTime,
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.primary,
),

View File

@@ -104,8 +104,13 @@ class _URLFormDialogState extends State<URLFormDialog> {
runSpacing: 16,
children: [
TextField(
maxLines: 5,
keyboardType: TextInputType.url,
minLines: 1,
maxLines: 5,
onSubmitted: (_) {
_handleAddProfileFormURL();
},
onEditingComplete: _handleAddProfileFormURL,
controller: urlController,
decoration: InputDecoration(
border: const OutlineInputBorder(),

View File

@@ -214,6 +214,7 @@ class _EditProfileState extends State<EditProfile> {
final items = [
ListItem(
title: TextFormField(
textInputAction: TextInputAction.next,
controller: labelController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
@@ -230,6 +231,8 @@ class _EditProfileState extends State<EditProfile> {
if (widget.profile.type == ProfileType.url) ...[
ListItem(
title: TextFormField(
textInputAction: TextInputAction.next,
keyboardType: TextInputType.url,
controller: urlController,
maxLines: 5,
minLines: 1,
@@ -258,6 +261,7 @@ class _EditProfileState extends State<EditProfile> {
if (autoUpdate)
ListItem(
title: TextFormField(
textInputAction: TextInputAction.next,
controller: autoUpdateDurationController,
decoration: InputDecoration(
border: const OutlineInputBorder(),

View File

@@ -23,7 +23,6 @@ class OverrideProfile extends StatefulWidget {
}
class _OverrideProfileState extends State<OverrideProfile> {
final GlobalKey<CacheItemExtentListViewState> _ruleListKey = GlobalKey();
final _controller = ScrollController();
double _currentMaxWidth = 0;
@@ -45,6 +44,7 @@ class _OverrideProfileState extends State<OverrideProfile> {
}
_handleSave(WidgetRef ref, OverrideData overrideData) {
ref.read(needApplyProvider.notifier).value = true;
ref.read(profilesProvider.notifier).updateProfile(
widget.profileId,
(state) => state.copyWith(
@@ -85,13 +85,6 @@ class _OverrideProfileState extends State<OverrideProfile> {
);
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_ruleListKey.currentState?.clearCache();
}
}
_buildContent() {
return Consumer(
builder: (_, ref, child) {
@@ -115,7 +108,7 @@ class _OverrideProfileState extends State<OverrideProfile> {
},
child: LayoutBuilder(
builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 104);
_currentMaxWidth = constraints.maxWidth - 104;
return CommonAutoHiddenScrollBar(
controller: _controller,
child: CustomScrollView(
@@ -147,7 +140,6 @@ class _OverrideProfileState extends State<OverrideProfile> {
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 0),
sliver: RuleContent(
maxWidth: _currentMaxWidth,
ruleListKey: _ruleListKey,
),
),
SliverToBoxAdapter(
@@ -227,7 +219,7 @@ class _OverrideProfileState extends State<OverrideProfile> {
message: TextSpan(
text: appLocalizations.saveTip,
),
confirmText: appLocalizations.tip,
confirmText: appLocalizations.save,
);
if (res != true) {
return;
@@ -313,8 +305,6 @@ class OverrideSwitch extends ConsumerWidget {
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 4,
bottom: 4,
),
title: Text(appLocalizations.enableOverride),
delegate: SwitchDelegate(
@@ -448,12 +438,10 @@ class RuleTitle extends ConsumerWidget {
}
class RuleContent extends ConsumerWidget {
final Key ruleListKey;
final double maxWidth;
const RuleContent({
super.key,
required this.ruleListKey,
required this.maxWidth,
});
@@ -601,7 +589,7 @@ class RuleContent extends ConsumerWidget {
);
}
return CacheItemExtentSliverReorderableList(
key: ruleListKey,
tag: CacheTag.rules,
itemBuilder: (context, index) {
final rule = rules[index];
return GestureDetector(
@@ -872,6 +860,8 @@ class _AddRuleDialogState extends State<AddRuleDialog> {
builder: (filed) {
return DropdownMenu(
width: 200,
enableFilter: false,
enableSearch: false,
controller: _subRuleController,
label: Text(appLocalizations.subRule),
menuHeight: 250,
@@ -889,11 +879,11 @@ class _AddRuleDialogState extends State<AddRuleDialog> {
builder: (filed) {
return DropdownMenu(
controller: _ruleTargetController,
initialSelection: filed.value,
label: Text(appLocalizations.ruleTarget),
width: 200,
menuHeight: 250,
enableFilter: true,
enableFilter: false,
enableSearch: false,
dropdownMenuEntries: _targetItems,
errorText: filed.errorText,
);

View File

@@ -349,12 +349,10 @@ class ProfileItem extends StatelessWidget {
),
PopupMenuItemData(
icon: Icons.delete_outlined,
iconSize: 20,
label: appLocalizations.delete,
onPressed: () {
_handleDeleteProfile(context);
},
type: PopupMenuItemType.danger,
),
],
),
@@ -370,7 +368,7 @@ class ProfileItem extends StatelessWidget {
),
),
title: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -63,7 +63,7 @@ class ProxyCard extends StatelessWidget {
delay > 0 ? '$delay ms' : "Timeout",
style: context.textTheme.labelSmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: other.getDelayColor(
color: utils.getDelayColor(
delay,
),
),

View File

@@ -6,7 +6,7 @@ import 'package:fl_clash/state.dart';
double get listHeaderHeight {
final measure = globalState.measure;
return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight;
return 20 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight;
}
double getItemHeight(ProxyCardType proxyCardType) {

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