Compare commits

...

6 Commits

Author SHA1 Message Date
chen08209
96a184a374 Add some scenes auto close connections
Support proxies search

Update core

Optimize more details
2025-05-17 20:40:19 +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
163 changed files with 7010 additions and 2711 deletions

View File

@@ -19,7 +19,7 @@ jobs:
os: windows-latest os: windows-latest
arch: amd64 arch: amd64
- platform: linux - platform: linux
os: ubuntu-latest os: ubuntu-22.04
arch: amd64 arch: amd64
- platform: macos - platform: macos
os: macos-13 os: macos-13
@@ -201,6 +201,7 @@ jobs:
env: env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
RUN_ID: ${{ github.run_id }}
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install requests pip install requests
@@ -211,6 +212,14 @@ jobs:
version=$(echo "${{ github.ref_name }}" | sed 's/^v//') version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md 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 - name: Release
if: ${{ env.IS_STABLE == 'true' }} if: ${{ env.IS_STABLE == 'true' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2

View File

@@ -1,3 +1,43 @@
## 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 ## v0.8.82
- Optimize android vpn performance - Optimize android vpn performance

View File

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

View File

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

View File

@@ -73,6 +73,9 @@ android {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
} }
release { release {
minifyEnabled true
debuggable false
if (isRelease) { if (isRelease) {
signingConfig signingConfigs.release signingConfig signingConfigs.release
} else { } else {

View File

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

View File

@@ -7,6 +7,10 @@ import com.follow.clash.plugins.VpnPlugin
import io.flutter.FlutterInjector import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.dart.DartExecutor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@@ -20,6 +24,10 @@ enum class RunState {
object GlobalState { object GlobalState {
val runLock = ReentrantLock() val runLock = ReentrantLock()
const val NOTIFICATION_CHANNEL = "FlClash"
const val NOTIFICATION_ID = 1
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP) val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null var flutterEngine: FlutterEngine? = null
private var serviceEngine: FlutterEngine? = null private var serviceEngine: FlutterEngine? = null
@@ -29,6 +37,15 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
} }
fun syncStatus() {
CoroutineScope(Dispatchers.Default).launch {
val status = getCurrentVPNPlugin()?.getStatus() ?: false
withContext(Dispatchers.Main){
runState.value = if (status) RunState.START else RunState.STOP
}
}
}
suspend fun getText(text: String): String { suspend fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: "" return getCurrentAppPlugin()?.getText(text) ?: ""
} }

View File

@@ -1,6 +1,5 @@
package com.follow.clash package com.follow.clash
import com.follow.clash.core.Core
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.TilePlugin
@@ -18,6 +17,7 @@ class MainActivity : FlutterActivity() {
override fun onDestroy() { override fun onDestroy() {
GlobalState.flutterEngine = null GlobalState.flutterEngine = null
GlobalState.runState.value = RunState.STOP
super.onDestroy() super.onDestroy()
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -168,8 +168,10 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
try { try {
if (GlobalState.runState.value != RunState.START) return if (GlobalState.runState.value != RunState.START) return
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams") val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
val startForegroundParams = Gson().fromJson( val startForegroundParams = if (data != null) Gson().fromJson(
data, StartForegroundParams::class.java data, StartForegroundParams::class.java
) else StartForegroundParams(
title = "", content = ""
) )
if (lastStartForegroundParams != startForegroundParams) { if (lastStartForegroundParams != startForegroundParams) {
lastStartForegroundParams = startForegroundParams lastStartForegroundParams = startForegroundParams
@@ -198,6 +200,13 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
timerJob = null timerJob = null
} }
suspend fun getStatus(): Boolean? {
return withContext(Dispatchers.Default) {
flutterMethodChannel.awaitResult<Boolean>("status", null)
}
}
private fun handleStartService() { private fun handleStartService() {
if (flClashService == null) { if (flClashService == null) {
bindService() bindService()
@@ -246,9 +255,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runLock.withLock { GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP GlobalState.runState.value = RunState.STOP
flClashService?.stop()
stopForegroundJob() stopForegroundJob()
Core.stopTun() Core.stopTun()
flClashService?.stop()
GlobalState.handleTryDestroy() GlobalState.handleTryDestroy()
} }
} }

View File

@@ -1,6 +1,26 @@
package com.follow.clash.services 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 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 { interface BaseServiceInterface {
@@ -9,4 +29,70 @@ interface BaseServiceInterface {
fun stop() fun stop()
suspend fun startForeground(title: String, content: String) 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 package com.follow.clash.services
import android.annotation.SuppressLint 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.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions 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 { 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() private val binder = LocalBinder()
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
@@ -38,93 +60,8 @@ class FlClashService : Service(), BaseServiceInterface {
return super.onUnbind(intent) return super.onUnbind(intent)
} }
private val CHANNEL = "FlClash" override fun onDestroy() {
stop()
private val notificationId: Int = 1 super.onDestroy()
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)
}
} }
} }

View File

@@ -33,6 +33,7 @@ class FlClashTileService : TileService() {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
GlobalState.syncStatus()
GlobalState.runState.value?.let { updateTile(it) } GlobalState.runState.value?.let { updateTile(it) }
GlobalState.runState.observeForever(observer) GlobalState.runState.observeForever(observer)
} }

View File

@@ -1,12 +1,7 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint 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.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.net.ProxyInfo import android.net.ProxyInfo
import android.net.VpnService import android.net.VpnService
import android.os.Binder import android.os.Binder
@@ -17,18 +12,13 @@ import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState 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.getIpv4RouteAddress
import com.follow.clash.extensions.getIpv6RouteAddress import com.follow.clash.extensions.getIpv6RouteAddress
import com.follow.clash.extensions.toCIDR import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -43,6 +33,10 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
if (options.ipv4Address.isNotEmpty()) { if (options.ipv4Address.isNotEmpty()) {
val cidr = options.ipv4Address.toCIDR() val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength) addAddress(cidr.address, cidr.prefixLength)
Log.d(
"addAddress",
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
val routeAddress = options.getIpv4RouteAddress() val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) { if (routeAddress.isNotEmpty()) {
try { try {
@@ -59,26 +53,39 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} else { } else {
addRoute("0.0.0.0", 0) addRoute("0.0.0.0", 0)
} }
} else {
addRoute("0.0.0.0", 0)
} }
if (options.ipv6Address.isNotEmpty()) { try {
val cidr = options.ipv6Address.toCIDR() if (options.ipv6Address.isNotEmpty()) {
addAddress(cidr.address, cidr.prefixLength) val cidr = options.ipv6Address.toCIDR()
val routeAddress = options.getIpv6RouteAddress() Log.d(
if (routeAddress.isNotEmpty()) { "addAddress6",
try { "address: ${cidr.address} prefixLength:${cidr.prefixLength}"
routeAddress.forEach { i -> )
Log.d( addAddress(cidr.address, cidr.prefixLength)
"addRoute6", val routeAddress = options.getIpv6RouteAddress()
"address: ${i.address} prefixLength:${i.prefixLength}" if (routeAddress.isNotEmpty()) {
) try {
addRoute(i.address, i.prefixLength) 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) addRoute("::", 0)
} }
} else {
addRoute("::", 0)
} }
}catch (_:Exception){
Log.d(
"addAddress6",
"IPv6 is not supported."
)
} }
addDnsServer(options.dnsServerAddress) addDnsServer(options.dnsServerAddress)
setMtu(9000) 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 suspend fun notificationBuilder(): NotificationCompat.Builder {
if (cachedBuilder == null) {
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy { cachedBuilder = createFlClashNotificationBuilder().await()
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)
}
} }
} return cachedBuilder!!
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await()
} }
@SuppressLint("ForegroundServiceType") @SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) { override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForeground(
val manager = getSystemService(NotificationManager::class.java) notificationBuilder()
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
getNotificationBuilder()
.setContentTitle(title) .setContentTitle(title)
.setContentText(content) .setContentText(content).build()
.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 onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {

View File

@@ -1,4 +1,4 @@
import com.android.build.gradle.tasks.MergeSourceSetFolders import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.argumentsWithVarargAsSingleArray
plugins { plugins {
id("com.android.library") id("com.android.library")
@@ -12,11 +12,11 @@ android {
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
consumerProguardFiles("consumer-rules.pro")
} }
buildTypes { buildTypes {
release { release {
isJniDebuggable = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
@@ -37,13 +37,17 @@ android {
} }
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { kotlinOptions {
jvmTarget = "11" 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") { val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
@@ -58,8 +62,4 @@ afterEvaluate {
tasks.named("preBuild") { tasks.named("preBuild") {
dependsOn(copyNativeLibs) dependsOn(copyNativeLibs)
} }
}
dependencies {
implementation("androidx.core:core-ktx:1.16.0")
} }

View File

@@ -4,6 +4,8 @@ project("core")
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}") message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
add_compile_options(-O3) add_compile_options(-O3)
@@ -21,6 +23,8 @@ if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
-Wl,--strip-all -Wl,--strip-all
-Wl,--exclude-libs=ALL -Wl,--exclude-libs=ALL
) )
add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden)
endif () endif ()
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so") set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")

View File

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

View File

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

View File

@@ -1,9 +1,6 @@
#pragma once #pragma once
#include <jni.h> #include <jni.h>
#include <cstdint>
#include <cstdlib>
#include <malloc.h>
struct scoped_jni { struct scoped_jni {
JNIEnv *env; JNIEnv *env;
@@ -18,14 +15,14 @@ extern char *jni_get_string(JNIEnv *env, jstring str);
extern int jni_catch_exception(JNIEnv *env); extern int jni_catch_exception(JNIEnv *env);
extern void jni_attach_thread(struct scoped_jni *jni); extern void jni_attach_thread(scoped_jni *jni);
extern void jni_detach_thread(struct scoped_jni *env); extern void jni_detach_thread(const scoped_jni *env);
extern void release_string(char **str); extern void release_string(char **str);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \ #define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
struct scoped_jni _jni; \ scoped_jni _jni{}; \
jni_attach_thread(&_jni); \ jni_attach_thread(&_jni); \
JNIEnv *env = _jni.env JNIEnv *env = _jni.env
@@ -36,4 +33,4 @@ extern void release_string(char **str);
#define new_global(obj) env->NewGlobalRef(obj) #define new_global(obj) env->NewGlobalRef(obj)
#define del_global(obj) env->DeleteGlobalRef(obj) #define del_global(obj) env->DeleteGlobalRef(obj)
#define get_string(jstr) jni_get_string(env, jstr) #define get_string(jstr) jni_get_string(env, jstr)
#define new_string(cstr) jni_new_string(env, cstr) #define new_string(cstr) jni_new_string(env, cstr)

View File

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

View File

@@ -385,5 +385,20 @@
"expressiveScheme": "Expressive", "expressiveScheme": "Expressive",
"contentScheme": "Content", "contentScheme": "Content",
"rainbowScheme": "Rainbow", "rainbowScheme": "Rainbow",
"fruitSaladScheme": "FruitSalad" "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

@@ -385,5 +385,21 @@
"expressiveScheme": "エクスプレッシブ", "expressiveScheme": "エクスプレッシブ",
"contentScheme": "コンテンツテーマ", "contentScheme": "コンテンツテーマ",
"rainbowScheme": "レインボー", "rainbowScheme": "レインボー",
"fruitSaladScheme": "フルーツサラダ" "fruitSaladScheme": "フルーツサラダ",
"developerMode": "デベロッパーモード",
"developerModeEnableTip": "デベロッパーモードが有効になりました。",
"messageTest": "メッセージテスト",
"messageTestTip": "これはメッセージです。",
"crashTest": "クラッシュテスト",
"clearData": "データを消去",
"zoom": "ズーム",
"textScale": "テキストスケーリング",
"internet": "インターネット",
"systemApp": "システムアプリ",
"noNetworkApp": "ネットワークなしアプリ",
"contactMe": "連絡する",
"recoveryStrategy": "リカバリー戦略",
"recoveryStrategy_override": "オーバーライド",
"recoveryStrategy_compatible": "互換性",
"logsTest": "ログテスト"
} }

View File

@@ -385,5 +385,21 @@
"expressiveScheme": "Экспрессивные", "expressiveScheme": "Экспрессивные",
"contentScheme": "Контентная тема", "contentScheme": "Контентная тема",
"rainbowScheme": "Радужные", "rainbowScheme": "Радужные",
"fruitSaladScheme": "Фруктовый микс" "fruitSaladScheme": "Фруктовый микс",
"developerMode": "Режим разработчика",
"developerModeEnableTip": "Режим разработчика активирован.",
"messageTest": "Тестирование сообщения",
"messageTestTip": "Это сообщение.",
"crashTest": "Тест на сбои",
"clearData": "Очистить данные",
"zoom": "Масштаб",
"textScale": "Масштабирование текста",
"internet": "Интернет",
"systemApp": "Системное приложение",
"noNetworkApp": "Приложение без сети",
"contactMe": "Свяжитесь со мной",
"recoveryStrategy": "Стратегия восстановления",
"recoveryStrategy_override": "Переопределение",
"recoveryStrategy_compatible": "Совместимый",
"logsTest": "Тест журналов"
} }

View File

@@ -385,5 +385,21 @@
"expressiveScheme": "表现力", "expressiveScheme": "表现力",
"contentScheme": "内容主题", "contentScheme": "内容主题",
"rainbowScheme": "彩虹", "rainbowScheme": "彩虹",
"fruitSaladScheme": "果缤纷" "fruitSaladScheme": "果缤纷",
"developerMode": "开发者模式",
"developerModeEnableTip": "开发者模式已启用。",
"messageTest": "消息测试",
"messageTestTip": "这是一条消息。",
"crashTest": "崩溃测试",
"clearData": "清除数据",
"zoom": "缩放",
"textScale": "文本缩放",
"internet": "互联网",
"systemApp": "系统应用",
"noNetworkApp": "无网络应用",
"contactMe": "联系我",
"recoveryStrategy": "恢复策略",
"recoveryStrategy_override": "覆盖",
"recoveryStrategy_compatible": "兼容",
"logsTest": "日志测试"
} }

View File

@@ -87,6 +87,9 @@ func handleAction(action *Action, result func(data interface{})) {
case closeConnectionsMethod: case closeConnectionsMethod:
result(handleCloseConnections()) result(handleCloseConnections())
return return
case resetConnectionsMethod:
result(handleResetConnections())
return
case closeConnectionMethod: case closeConnectionMethod:
id := action.Data.(string) id := action.Data.(string)
result(handleCloseConnection(id)) result(handleCloseConnection(id))
@@ -166,6 +169,9 @@ func handleAction(action *Action, result func(data interface{})) {
data := action.Data.(string) data := action.Data.(string)
handleSetState(data) handleSetState(data)
result(true) result(true)
case crashMethod:
result(true)
handleCrash()
default: default:
handle := nextHandle(action, result) handle := nextHandle(action, result)
if handle { if handle {

View File

@@ -43,13 +43,13 @@ var (
} }
) )
func protect(callback unsafe.Pointer, fd int) { func Protect(callback unsafe.Pointer, fd int) {
if globalCallbacks.protectFunc != nil { if globalCallbacks.protectFunc != nil {
C.protect(globalCallbacks.protectFunc, callback, C.int(fd)) C.protect(globalCallbacks.protectFunc, callback, C.int(fd))
} }
} }
func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string { func ResolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
if globalCallbacks.resolveProcessFunc == nil { if globalCallbacks.resolveProcessFunc == nil {
return "" return ""
} }

View File

@@ -78,7 +78,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
path := getProfilePath(id) path := getProfilePath(id)
bytes, err := readFile(path) bytes, err := readFile(path)
if err != nil { if err != nil {
log.Errorln("profile is not exist")
return config.DefaultRawConfig() return config.DefaultRawConfig()
} }
prof, err := config.UnmarshalRawConfig(bytes) prof, err := config.UnmarshalRawConfig(bytes)
@@ -238,6 +237,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.Tun.AutoRoute = patchConfig.Tun.AutoRoute
targetConfig.GeodataLoader = patchConfig.GeodataLoader targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl targetConfig.GeoXUrl = patchConfig.GeoXUrl
@@ -362,10 +362,11 @@ func applyConfig(rawConfig *config.RawConfig) error {
} }
if configParams.IsPatch { if configParams.IsPatch {
patchConfig() patchConfig()
updateListeners(false)
} else { } else {
hub.ApplyConfig(currentConfig) hub.ApplyConfig(currentConfig)
patchSelectGroup() patchSelectGroup()
updateListeners(true)
} }
updateListeners(false)
return err return err
} }

View File

@@ -64,6 +64,7 @@ const (
asyncTestDelayMethod Method = "asyncTestDelay" asyncTestDelayMethod Method = "asyncTestDelay"
getConnectionsMethod Method = "getConnections" getConnectionsMethod Method = "getConnections"
closeConnectionsMethod Method = "closeConnections" closeConnectionsMethod Method = "closeConnections"
resetConnectionsMethod Method = "resetConnectionsMethod"
closeConnectionMethod Method = "closeConnection" closeConnectionMethod Method = "closeConnection"
getExternalProvidersMethod Method = "getExternalProviders" getExternalProvidersMethod Method = "getExternalProviders"
getExternalProviderMethod Method = "getExternalProvider" getExternalProviderMethod Method = "getExternalProvider"
@@ -82,6 +83,7 @@ const (
getRunTimeMethod Method = "getRunTime" getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName" getCurrentProfileNameMethod Method = "getCurrentProfileName"
getProfileMethod Method = "getProfile" getProfileMethod Method = "getProfile"
crashMethod Method = "crash"
) )
type Method string type Method string

View File

@@ -6,12 +6,13 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require ( require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000 github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.49.1 github.com/samber/lo v1.50.0
golang.org/x/sync v0.11.0
) )
require ( require (
github.com/3andne/restls-client-go v0.1.6 // indirect github.com/3andne/restls-client-go v0.1.6 // indirect
github.com/RyuaNerin/go-krypto v1.2.4 // indirect github.com/RyuaNerin/go-krypto v1.3.0 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
@@ -20,13 +21,13 @@ require (
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.2 // indirect github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 // indirect
github.com/enfein/mieru/v3 v3.13.0 // indirect github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect github.com/go-chi/render v1.0.3 // indirect
@@ -35,7 +36,7 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.1 // indirect github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
@@ -52,20 +53,25 @@ require (
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.19.0 // indirect github.com/metacubex/bart v0.19.0 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.1 // indirect github.com/metacubex/chacha v0.1.2 // indirect
github.com/metacubex/fswatch v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // 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/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 // indirect
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect github.com/metacubex/sing-mux v0.3.2 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect github.com/metacubex/sing-shadowsocks v0.2.9 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.3 // indirect
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6 // indirect
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect github.com/metacubex/sing-vmess v0.2.1 // indirect
github.com/metacubex/utls v1.6.8-alpha.4 // indirect github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf // indirect
github.com/metacubex/utls v1.7.0-alpha.3 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // indirect github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
@@ -77,15 +83,8 @@ require (
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.2 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
@@ -107,7 +106,6 @@ require (
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.35.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/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.7.0 // indirect

View File

@@ -1,8 +1,8 @@
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A= github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go= github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -26,12 +26,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 h1:W7mpP4uiOAbBOdDnRXT9EUdauFv7bz+ERT5rPIord00=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98= github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
@@ -39,8 +39,8 @@ github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBE
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0= github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -101,34 +101,45 @@ 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/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c= github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b h1:8oDU32eJ+RRhl1FodGgPfxQxtoBAiD9D40XG2XtU/SE=
github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b/go.mod h1:9R1NOzCgTcWsdWvOMlmtMuF0uKzuOpsfvEf7U3I8zM0=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= 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/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8= github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 h1:m4nSxvw46JEgxMzzmnXams+ebwabcry4Ydep/zNiesQ=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA= github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 h1:wfmYgtECbEYo1slMtyo+2kMqscYYDSjU/TVgS3018F4=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2/go.mod h1:P1kd57U6XXmXv9PbwWdznUGT0k9bKgFJXF0fEORbIlk=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg= github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8= github.com/metacubex/sing-shadowsocks2 v0.2.3/go.mod h1:/WNy/Q8ahLCoPRriWuFZFD0Jy+JNp1MEQl28Zw6SaF8=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6 h1:TAwL91XPa6x1QK55CRm+VTzPvLPUfEr/uFDnOZArqEU=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/sing-vmess v0.2.1 h1:I6gM3VUjtvJ15D805EUbNH+SRBuqzJeFnuIbKYUsWZ0=
github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI= github.com/metacubex/sing-vmess v0.2.1/go.mod h1:DsODWItJtOMZNna8Qhheg8r3tUivrcO3vWgaTYKnfTo=
github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf h1:LwID1wz4tzypidd412dd4dC1H0m1TgRCQ/XvRvMJDFM=
github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.0-alpha.3 h1:cp1cEMUnoifiWrGHRzo+nCwPRveN9yPD8QaRFmfcYxA=
github.com/metacubex/utls v1.7.0-alpha.3/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 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
@@ -159,27 +170,12 @@ github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
@@ -191,9 +187,16 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
@@ -253,7 +256,7 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=

View File

@@ -10,6 +10,7 @@ import (
"github.com/metacubex/mihomo/common/observable" "github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
@@ -53,6 +54,7 @@ func handleStartListener() bool {
defer runLock.Unlock() defer runLock.Unlock()
isRunning = true isRunning = true
updateListeners(true) updateListeners(true)
resolver.ResetConnection()
return true return true
} }
@@ -266,6 +268,11 @@ func handleGetConnections() string {
func handleCloseConnections() bool { func handleCloseConnections() bool {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
closeConnections()
return true
}
func closeConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool { statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close() err := c.Close()
if err != nil { if err != nil {
@@ -273,6 +280,12 @@ func handleCloseConnections() bool {
} }
return true return true
}) })
}
func handleResetConnections() bool {
runLock.Lock()
defer runLock.Unlock()
resolver.ResetConnection()
return true return true
} }
@@ -442,6 +455,10 @@ func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState) _ = json.Unmarshal([]byte(params), state.CurrentState)
} }
func handleCrash() {
panic("handle invoke crash")
}
func init() { func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) { adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{ delayData := &Delay{

View File

@@ -58,7 +58,7 @@ func (t *TunHandler) handleProtect(fd int) {
return return
} }
protect(t.callback, fd) Protect(t.callback, fd)
} }
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string { func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
@@ -79,7 +79,7 @@ func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
if version < 29 { if version < 29 {
uid = platform.QuerySocketUidFromProcFs(source, target) uid = platform.QuerySocketUidFromProcFs(source, target)
} }
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid) return ResolveProcess(t.callback, protocol, source.String(), target.String(), uid)
} }
var ( var (
@@ -98,13 +98,13 @@ func handleStopTun() {
} }
} }
func handleStartTun(fd int, callback unsafe.Pointer) bool { func handleStartTun(fd int, callback unsafe.Pointer) {
handleStopTun() handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now() now := time.Now()
runTime = &now runTime = &now
if fd != 0 { if fd != 0 {
tunLock.Lock()
defer tunLock.Unlock()
tunHandler = &TunHandler{ tunHandler = &TunHandler{
callback: callback, callback: callback,
limit: semaphore.NewWeighted(4), limit: semaphore.NewWeighted(4),
@@ -113,13 +113,11 @@ func handleStartTun(fd int, callback unsafe.Pointer) bool {
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack) tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil { if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address()) log.Infoln("TUN address: %v", tunListener.Address())
tunHandler.listener = tunListener
} else { } else {
removeTunHook() removeTunHook()
return false
} }
tunHandler.listener = tunListener
} }
return true
} }
func handleGetRunTime() string { func handleGetRunTime() string {
@@ -228,7 +226,10 @@ func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.c
//export startTUN //export startTUN
func startTUN(fd C.int, callback unsafe.Pointer) bool { func startTUN(fd C.int, callback unsafe.Pointer) bool {
return handleStartTun(int(fd), callback) go func() {
handleStartTun(int(fd), callback)
}()
return true
} }
//export getRunTime //export getRunTime
@@ -238,7 +239,9 @@ func getRunTime() *C.char {
//export stopTun //export stopTun
func stopTun() { func stopTun() {
handleStopTun() go func() {
handleStopTun()
}()
} }
//export getCurrentProfileName //export getCurrentProfileName

View File

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

View File

@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/l10n/l10n.dart';
@@ -14,7 +13,6 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'controller.dart'; import 'controller.dart';
import 'models/models.dart';
import 'pages/pages.dart'; import 'pages/pages.dart';
class Application extends ConsumerStatefulWidget { class Application extends ConsumerStatefulWidget {
@@ -27,7 +25,6 @@ class Application extends ConsumerStatefulWidget {
} }
class ApplicationState extends ConsumerState<Application> { class ApplicationState extends ConsumerState<Application> {
late ColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer; Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer; Timer? _autoUpdateProfilesTaskTimer;
@@ -103,7 +100,8 @@ class ApplicationState extends ConsumerState<Application> {
return AppStateManager( return AppStateManager(
child: ClashManager( child: ClashManager(
child: ConnectivityManager( child: ConnectivityManager(
onConnectivityChanged: () { onConnectivityChanged: () async {
await clashCore.resetConnections();
globalState.appController.updateLocalIp(); globalState.appController.updateLocalIp();
globalState.appController.addCheckIpNumDebounce(); globalState.appController.addCheckIpNumDebounce();
}, },
@@ -132,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 @override
Widget build(context) { Widget build(context) {
return _buildPlatformState( return _buildPlatformState(
@@ -154,49 +139,44 @@ class ApplicationState extends ConsumerState<Application> {
final locale = final locale =
ref.watch(appSettingProvider.select((state) => state.locale)); ref.watch(appSettingProvider.select((state) => state.locale));
final themeProps = ref.watch(themeSettingProvider); final themeProps = ref.watch(themeSettingProvider);
return DynamicColorBuilder( return MaterialApp(
builder: (lightDynamic, darkDynamic) { debugShowCheckedModeBanner: false,
_updateSystemColorSchemes(lightDynamic, darkDynamic); navigatorKey: globalState.navigatorKey,
return MaterialApp( localizationsDelegates: const [
debugShowCheckedModeBanner: false, AppLocalizations.delegate,
navigatorKey: globalState.navigatorKey, GlobalMaterialLocalizations.delegate,
localizationsDelegates: const [ GlobalCupertinoLocalizations.delegate,
AppLocalizations.delegate, GlobalWidgetsLocalizations.delegate
GlobalMaterialLocalizations.delegate, ],
GlobalCupertinoLocalizations.delegate, builder: (_, child) {
GlobalWidgetsLocalizations.delegate return AppEnvManager(
], child: _buildPlatformApp(
builder: (_, child) { _buildApp(child!),
return AppEnvManager(
child: _buildPlatformApp(
_buildApp(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,
); );
}, },
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(), child: const HomePage(),

View File

@@ -143,6 +143,10 @@ class ClashCore {
clashInterface.closeConnections(); clashInterface.closeConnections();
} }
resetConnections() {
clashInterface.resetConnections();
}
Future<List<ExternalProvider>> getExternalProviders() async { Future<List<ExternalProvider>> getExternalProviders() async {
final externalProvidersRawString = final externalProvidersRawString =
await clashInterface.getExternalProviders(); await clashInterface.getExternalProviders();

View File

@@ -58,12 +58,16 @@ mixin ClashInterface {
stopLog(); stopLog();
Future<bool> crash();
FutureOr<String> getConnections(); FutureOr<String> getConnections();
FutureOr<bool> closeConnection(String id); FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections(); FutureOr<bool> closeConnections();
FutureOr<bool> resetConnections();
FutureOr<String> getProfile(String id); FutureOr<String> getProfile(String id);
Future<bool> setState(CoreState state); Future<bool> setState(CoreState state);
@@ -104,6 +108,7 @@ abstract class ClashHandlerInterface with ClashInterface {
case ActionMethod.closeConnection: case ActionMethod.closeConnection:
case ActionMethod.stopListener: case ActionMethod.stopListener:
case ActionMethod.setState: case ActionMethod.setState:
case ActionMethod.crash:
completer?.complete(result.data as bool); completer?.complete(result.data as bool);
return; return;
case ActionMethod.changeProxy: case ActionMethod.changeProxy:
@@ -242,6 +247,13 @@ abstract class ClashHandlerInterface with ClashInterface {
); );
} }
@override
Future<bool> crash() {
return invoke<bool>(
method: ActionMethod.crash,
);
}
@override @override
Future<String> getProxies() { Future<String> getProxies() {
return invoke<String>( return invoke<String>(
@@ -318,6 +330,13 @@ abstract class ClashHandlerInterface with ClashInterface {
); );
} }
@override
Future<bool> resetConnections() {
return invoke<bool>(
method: ActionMethod.resetConnections,
);
}
@override @override
Future<bool> closeConnection(String id) { Future<bool> closeConnection(String id) {
return invoke<bool>( return invoke<bool>(

View File

@@ -267,6 +267,15 @@ class ClashLibHandler {
return true; return true;
} }
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
if (runTimeString.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
Future<String> quickStart( Future<String> quickStart(
InitParams initParams, InitParams initParams,
UpdateConfigParams updateConfigParams, UpdateConfigParams updateConfigParams,
@@ -297,15 +306,10 @@ class ClashLibHandler {
malloc.free(stateParamsChar); malloc.free(stateParamsChar);
return completer.future; return completer.future;
} }
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
} }
ClashLib? get clashLib => ClashLib? get clashLib =>
Platform.isAndroid && !globalState.isService ? ClashLib() : null; Platform.isAndroid && !globalState.isService ? ClashLib() : null;
ClashLibHandler? get clashLibHandler =>
Platform.isAndroid ? ClashLibHandler() : null;

View File

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

View File

@@ -4,36 +4,37 @@ export 'color.dart';
export 'constant.dart'; export 'constant.dart';
export 'context.dart'; export 'context.dart';
export 'datetime.dart'; export 'datetime.dart';
export 'fixed.dart';
export 'function.dart'; export 'function.dart';
export 'future.dart'; export 'future.dart';
export 'http.dart'; export 'http.dart';
export 'icons.dart'; export 'icons.dart';
export 'iterable.dart'; export 'iterable.dart';
export 'javascript.dart';
export 'keyboard.dart'; export 'keyboard.dart';
export 'launch.dart'; export 'launch.dart';
export 'link.dart'; export 'link.dart';
export 'list.dart';
export 'lock.dart'; export 'lock.dart';
export 'measure.dart'; export 'measure.dart';
export 'mixin.dart';
export 'navigation.dart'; export 'navigation.dart';
export 'navigator.dart'; export 'navigator.dart';
export 'network.dart'; export 'network.dart';
export 'num.dart'; export 'num.dart';
export 'utils.dart';
export 'package.dart'; export 'package.dart';
export 'path.dart'; export 'path.dart';
export 'picker.dart'; export 'picker.dart';
export 'preferences.dart'; export 'preferences.dart';
export 'print.dart';
export 'protocol.dart'; export 'protocol.dart';
export 'proxy.dart'; export 'proxy.dart';
export 'render.dart';
export 'request.dart'; export 'request.dart';
export 'scroll.dart'; export 'scroll.dart';
export 'string.dart'; export 'string.dart';
export 'system.dart'; export 'system.dart';
export 'text.dart'; export 'text.dart';
export 'tray.dart'; export 'tray.dart';
export 'utils.dart';
export 'window.dart'; export 'window.dart';
export 'windows.dart'; export 'windows.dart';
export 'render.dart';
export 'mixin.dart';
export 'print.dart';

View File

@@ -16,16 +16,15 @@ const browserUa =
const packageName = "com.follow.clash"; const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock"; final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890; const helperPort = 47890;
const helperTag = "2024125"; const maxTextScale = 1.4;
const baseInfoEdgeInsets = EdgeInsets.symmetric( const minTextScale = 0.8;
vertical: 16, final baseInfoEdgeInsets = EdgeInsets.symmetric(
horizontal: 16, vertical: 16.ap,
horizontal: 16.ap,
); );
double textScaleFactor = min( final defaultTextScaleFactor =
WidgetsBinding.instance.platformDispatcher.textScaleFactor, WidgetsBinding.instance.platformDispatcher.textScaleFactor;
1.2,
);
const httpTimeoutDuration = Duration(milliseconds: 5000); const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100); const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100);
@@ -44,7 +43,6 @@ const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1"; const localhost = "127.0.0.1";
const clashConfigKey = "clash_config"; const clashConfigKey = "clash_config";
const configKey = "config"; const configKey = "config";
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
const double dialogCommonWidth = 300; const double dialogCommonWidth = 300;
const repository = "chen08209/FlClash"; const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090"; const defaultExternalController = "127.0.0.1:9090";
@@ -60,6 +58,7 @@ final commonFilter = ImageFilter.blur(
const navigationItemListEquality = ListEquality<NavigationItem>(); const navigationItemListEquality = ListEquality<NavigationItem>();
const connectionListEquality = ListEquality<Connection>(); const connectionListEquality = ListEquality<Connection>();
const stringListEquality = ListEquality<String>(); const stringListEquality = ListEquality<String>();
const intListEquality = ListEquality<int>();
const logListEquality = ListEquality<Log>(); const logListEquality = ListEquality<Log>();
const groupListEquality = ListEquality<Group>(); const groupListEquality = ListEquality<Group>();
const externalProviderListEquality = ListEquality<ExternalProvider>(); const externalProviderListEquality = ListEquality<ExternalProvider>();
@@ -78,22 +77,28 @@ const viewModeColumnsMap = {
ViewMode.desktop: [4, 3], ViewMode.desktop: [4, 3],
}; };
const defaultPrimaryColor = 0xFF795548; // const proxiesStoreKey = PageStorageKey<String>('proxies');
// const toolsStoreKey = PageStorageKey<String>('tools');
// const profilesStoreKey = PageStorageKey<String>('profiles');
const defaultPrimaryColor = 0XFFD8C0C3;
double getWidgetHeight(num lines) { 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 mainIsolate = "FlClashMainIsolate";
final serviceIsolate = "FlClashServiceIsolate"; final serviceIsolate = "FlClashServiceIsolate";
const defaultPrimaryColors = [ const defaultPrimaryColors = [
defaultPrimaryColor, 0xFF795548,
0xFF03A9F4, 0xFF03A9F4,
0xFFFFFF00, 0xFFFFFF00,
0XFFBBC9CC, 0XFFBBC9CC,
0XFFABD397, 0XFFABD397,
0XFFD8C0C3, defaultPrimaryColor,
0XFF665390, 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:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -11,6 +11,36 @@ extension BuildContextExtension on BuildContext {
return findAncestorStateOfType<MessageManagerState>()?.message(text); 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 { Size get appSize {
return MediaQuery.of(this).size; return MediaQuery.of(this).size;
} }
@@ -27,10 +57,10 @@ extension BuildContextExtension on BuildContext {
T? state; T? state;
visitor(Element element) { visitor(Element element) {
if(!element.mounted){ if (!element.mounted) {
return; return;
} }
if(element is StatefulElement){ if (element is StatefulElement) {
if (element.state is T) { if (element.state is T) {
state = element.state as 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 'dart:async';
import 'package:fl_clash/enum/enum.dart';
class Debouncer { class Debouncer {
final Map<dynamic, Timer?> _operations = {}; final Map<FunctionTag, Timer?> _operations = {};
call( call(
dynamic tag, FunctionTag tag,
Function func, { Function func, {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
@@ -33,10 +35,10 @@ class Debouncer {
} }
class Throttler { class Throttler {
final Map<dynamic, Timer?> _operations = {}; final Map<FunctionTag, Timer?> _operations = {};
call( call(
dynamic tag, FunctionTag tag,
Function func, { Function func, {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),

View File

@@ -38,6 +38,43 @@ extension IterableExt<T> on Iterable<T> {
count++; 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> { extension DoubleListExt on List<double> {
@@ -67,9 +104,9 @@ extension DoubleListExt on List<double> {
} }
extension MapExt<K, V> on Map<K, V> { extension MapExt<K, V> on Map<K, V> {
getCacheValue(K key, V defaultValue) { updateCacheValue(K key, V Function() callback) {
if (this[key] == null) { if (this[key] == null) {
this[key] = defaultValue; this[key] = callback();
} }
return this[key]; return this[key];
} }

View File

@@ -0,0 +1,22 @@
import 'package:flutter_js/flutter_js.dart';
class Javascript {
static Javascript? _instance;
late JavascriptRuntime runtime;
Javascript._internal() {
runtime = getJavascriptRuntime();
}
Future<String> evaluate(String js) async {
final evaluate = runtime.evaluate(js);
return evaluate.stringResult;
}
factory Javascript() {
_instance ??= Javascript._internal();
return _instance!;
}
}
final js = Javascript();

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

View File

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

View File

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

View File

@@ -73,14 +73,12 @@ class AppPath {
return join(directory.path, profilesDirectoryName); return join(directory.path, profilesDirectoryName);
} }
Future<String?> getProfilePath(String? id) async { Future<String> getProfilePath(String id) async {
if (id == null) return null;
final directory = await profilesPath; final directory = await profilesPath;
return join(directory, "$id.yaml"); return join(directory, "$id.yaml");
} }
Future<String?> getProvidersPath(String? id) async { Future<String> getProvidersPath(String id) async {
if (id == null) return null;
final directory = await profilesPath; final directory = await profilesPath;
return join(directory, "providers", id); return join(directory, "providers", id);
} }

View File

@@ -1,4 +1,3 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@@ -16,14 +15,11 @@ class CommonPrint {
log(String? text) { log(String? text) {
final payload = "[FlClash] $text"; final payload = "[FlClash] $text";
debugPrint(payload); debugPrint(payload);
if (globalState.isService) { if (!globalState.isInit) {
return; return;
} }
globalState.appController.addLog( globalState.appController.addLog(
Log( Log.app(payload),
logLevel: LogLevel.info,
payload: payload,
),
); );
} }
} }

View File

@@ -23,14 +23,14 @@ class Render {
pause() { pause() {
throttler.call( throttler.call(
DebounceTag.renderPause, FunctionTag.renderPause,
_pause, _pause,
duration: Duration(seconds: 5), duration: Duration(seconds: 5),
); );
} }
resume() { resume() {
throttler.cancel(DebounceTag.renderPause); throttler.cancel(FunctionTag.renderPause);
_resume(); _resume();
} }

View File

@@ -130,7 +130,7 @@ class Request {
if (response.statusCode != HttpStatus.ok) { if (response.statusCode != HttpStatus.ok) {
return false; return false;
} }
return (response.data as String) == helperTag; return (response.data as String) == globalState.coreSHA256;
} catch (_) { } catch (_) {
return false; return false;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,9 @@ import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lpinyin/lpinyin.dart'; import 'package:lpinyin/lpinyin.dart';
import 'package:yaml/yaml.dart';
class Utils { class Utils {
Color? getDelayColor(int? delay) { Color? getDelayColor(int? delay) {
@@ -230,7 +232,7 @@ class Utils {
} }
int getProfilesColumns(double viewWidth) { int getProfilesColumns(double viewWidth) {
return max((viewWidth / 350).floor(), 1); return max((viewWidth / 320).floor(), 1);
} }
final _indexPrimary = [ final _indexPrimary = [
@@ -323,6 +325,40 @@ class Utils {
} }
return ""; return "";
} }
SingleActivator controlSingleActivator(LogicalKeyboardKey trigger) {
final control = Platform.isMacOS ? false : true;
return SingleActivator(
trigger,
control: control,
meta: !control,
);
}
dynamic convertLoadYaml(dynamic node) {
if (node is YamlMap) {
final map = <String, dynamic>{};
node.nodes.forEach((key, value) {
String stringKey;
if (key is YamlScalar) {
stringKey = key.value.toString();
} else {
stringKey = key.toString();
}
map[stringKey] = convertLoadYaml(value.value);
});
return map;
} else if (node is YamlList) {
final list = <dynamic>[];
for (final item in node.nodes) {
list.add(convertLoadYaml(item.value));
}
return list;
} else if (node is YamlScalar) {
return node.value;
}
return node;
}
} }
final utils = Utils(); 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/clash/clash.dart';
import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart'; import 'package:fl_clash/widgets/dialog.dart';
@@ -30,18 +31,18 @@ class AppController {
AppController(this.context, WidgetRef ref) : _ref = ref; AppController(this.context, WidgetRef ref) : _ref = ref;
updateClashConfigDebounce() { updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, () async { debouncer.call(FunctionTag.updateClashConfig, () async {
final isPatch = globalState.appState.needApply ? false : true; final isPatch = globalState.appState.needApply ? false : true;
await updateClashConfig(isPatch); await updateClashConfig(isPatch);
}); });
} }
updateGroupsDebounce() { updateGroupsDebounce() {
debouncer.call(DebounceTag.updateGroups, updateGroups); debouncer.call(FunctionTag.updateGroups, updateGroups);
} }
addCheckIpNumDebounce() { addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () { debouncer.call(FunctionTag.addCheckIpNum, () {
_ref.read(checkIpNumProvider.notifier).add(); _ref.read(checkIpNumProvider.notifier).add();
}); });
} }
@@ -49,17 +50,17 @@ class AppController {
applyProfileDebounce({ applyProfileDebounce({
bool silence = false, bool silence = false,
}) { }) {
debouncer.call(DebounceTag.applyProfile, (silence) { debouncer.call(FunctionTag.applyProfile, (silence) {
applyProfile(silence: silence); applyProfile(silence: silence);
}, args: [silence]); }, args: [silence]);
} }
savePreferencesDebounce() { savePreferencesDebounce() {
debouncer.call(DebounceTag.savePreferences, savePreferences); debouncer.call(FunctionTag.savePreferences, savePreferences);
} }
changeProxyDebounce(String groupName, String proxyName) { changeProxyDebounce(String groupName, String proxyName) {
debouncer.call(DebounceTag.changeProxy, debouncer.call(FunctionTag.changeProxy,
(String groupName, String proxyName) async { (String groupName, String proxyName) async {
await changeProxy( await changeProxy(
groupName: groupName, groupName: groupName,
@@ -260,9 +261,7 @@ class AppController {
final patchConfig = _ref.read(patchClashConfigProvider); final patchConfig = _ref.read(patchClashConfigProvider);
final appSetting = _ref.read(appSettingProvider); final appSetting = _ref.read(appSettingProvider);
bool enableTun = patchConfig.tun.enable; bool enableTun = patchConfig.tun.enable;
if (enableTun != lastTunEnable && if (enableTun != lastTunEnable && lastTunEnable == false) {
lastTunEnable == false &&
!Platform.isAndroid) {
final code = await system.authorizeCore(); final code = await system.authorizeCore();
switch (code) { switch (code) {
case AuthorizeCode.none: case AuthorizeCode.none:
@@ -314,6 +313,10 @@ class AppController {
handleChangeProfile() { handleChangeProfile() {
_ref.read(delayDataSourceProvider.notifier).value = {}; _ref.read(delayDataSourceProvider.notifier).value = {};
applyProfile(); applyProfile();
_ref.read(logsProvider.notifier).value = FixedList(500);
_ref.read(requestsProvider.notifier).value = FixedList(500);
globalState.cacheHeightMap = {};
globalState.cacheScrollPosition = {};
} }
updateBrightness(Brightness brightness) { updateBrightness(Brightness brightness) {
@@ -334,23 +337,22 @@ class AppController {
try { try {
await updateProfile(profile); await updateProfile(profile);
} catch (e) { } catch (e) {
_ref.read(logsProvider.notifier).addLog( commonPrint.log(e.toString());
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
} }
} }
} }
Future<void> updateGroups() async { Future<void> updateGroups() async {
_ref.read(groupsProvider.notifier).value = await retry( try {
task: () async { _ref.read(groupsProvider.notifier).value = await retry(
return await clashCore.getProxiesGroups(); task: () async {
}, return await clashCore.getProxiesGroups();
retryIf: (res) => res.isEmpty, },
); retryIf: (res) => res.isEmpty,
);
} catch (_) {
_ref.read(groupsProvider.notifier).value = [];
}
} }
updateProfiles() async { updateProfiles() async {
@@ -362,10 +364,6 @@ class AppController {
} }
} }
updateSystemColorSchemes(ColorSchemes colorSchemes) {
_ref.read(appSchemesProvider.notifier).value = colorSchemes;
}
savePreferences() async { savePreferences() async {
commonPrint.log("save preferences"); commonPrint.log("save preferences");
await preferences.saveConfig(globalState.config); await preferences.saveConfig(globalState.config);
@@ -382,12 +380,15 @@ class AppController {
), ),
); );
if (_ref.read(appSettingProvider).closeConnections) { if (_ref.read(appSettingProvider).closeConnections) {
clashCore.closeConnections(); clashCore.resetConnections();
} }
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
handleBackOrExit() async { handleBackOrExit() async {
if (_ref.read(backBlockProvider)) {
return;
}
if (_ref.read(appSettingProvider).minimizeOnExit) { if (_ref.read(appSettingProvider).minimizeOnExit) {
if (system.isDesktop) { if (system.isDesktop) {
await savePreferencesDebounce(); await savePreferencesDebounce();
@@ -398,18 +399,34 @@ class AppController {
} }
} }
backBlock() {
_ref.read(backBlockProvider.notifier).value = true;
}
unBackBlock() {
_ref.read(backBlockProvider.notifier).value = false;
}
handleExit() async { handleExit() async {
try { try {
await updateStatus(false); await updateStatus(false);
await proxy?.stopProxy();
await clashCore.shutdown(); await clashCore.shutdown();
await clashService?.destroy(); await clashService?.destroy();
await proxy?.stopProxy();
await savePreferences(); await savePreferences();
} finally { } finally {
system.exit(); system.exit();
} }
} }
Future handleClear() async {
await preferences.clearPreferences();
commonPrint.log("clear preferences");
globalState.config = Config(
themeProps: defaultThemeProps,
);
}
autoCheckUpdate() async { autoCheckUpdate() async {
if (!_ref.read(appSettingProvider).autoCheckUpdate) return; if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
final res = await request.checkForUpdate(); final res = await request.checkForUpdate();
@@ -484,17 +501,18 @@ class AppController {
Future<void> _initCore() async { Future<void> _initCore() async {
final isInit = await clashCore.isInit; final isInit = await clashCore.isInit;
if (!isInit) { if (!isInit) {
await clashCore.init();
await clashCore.setState( await clashCore.setState(
globalState.getCoreState(), globalState.getCoreState(),
); );
await clashCore.init();
} }
await applyProfile(); await applyProfile();
} }
init() async { init() async {
await _handlePreference(); FlutterError.onError = (details) {
await _handlerDisclaimer(); commonPrint.log(details.stack.toString());
};
await _initCore(); await _initCore();
await _initStatus(); await _initStatus();
updateTray(true); updateTray(true);
@@ -508,6 +526,8 @@ class AppController {
} else { } else {
window?.hide(); window?.hide();
} }
await _handlePreference();
await _handlerDisclaimer();
_ref.read(initProvider.notifier).value = true; _ref.read(initProvider.notifier).value = true;
} }
@@ -685,10 +705,16 @@ class AppController {
return List.of(proxies) return List.of(proxies)
..sort( ..sort(
(a, b) { (a, b) {
final aDelay = final aDelay = _ref.read(getDelayProvider(
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl)); proxyName: a.name,
final bDelay = testUrl: testUrl,
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl)); ));
final bDelay = _ref.read(
getDelayProvider(
proxyName: b.name,
testUrl: testUrl,
),
);
if (aDelay == null && bDelay == null) { if (aDelay == null && bDelay == null) {
return 0; return 0;
} }
@@ -718,19 +744,15 @@ class AppController {
final profilePath = await appPath.getProfilePath(profileId); final profilePath = await appPath.getProfilePath(profileId);
final providersPath = await appPath.getProvidersPath(profileId); final providersPath = await appPath.getProvidersPath(profileId);
return await Isolate.run(() async { return await Isolate.run(() async {
if (profilePath != null) { final profileFile = File(profilePath);
final profileFile = File(profilePath); final isExists = await profileFile.exists();
final isExists = await profileFile.exists(); if (isExists) {
if (isExists) { profileFile.delete(recursive: true);
profileFile.delete(recursive: true);
}
} }
if (providersPath != null) { final providersFileDir = File(providersPath);
final providersFileDir = File(providersPath); final providersFileIsExists = await providersFileDir.exists();
final isExists = await providersFileDir.exists(); if (providersFileIsExists) {
if (isExists) { providersFileDir.delete(recursive: true);
providersFileDir.delete(recursive: true);
}
} }
}); });
} }
@@ -749,6 +771,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() { updateStart() {
updateStatus(!_ref.read(runTimeProvider.notifier).isStart); updateStatus(!_ref.read(runTimeProvider.notifier).isStart);
} }
@@ -937,30 +970,39 @@ class AppController {
} }
_recovery(Config config, RecoveryOption recoveryOption) { _recovery(Config config, RecoveryOption recoveryOption) {
final recoveryStrategy = _ref.read(appSettingProvider.select(
(state) => state.recoveryStrategy,
));
final profiles = config.profiles; final profiles = config.profiles;
for (final profile in profiles) { if (recoveryStrategy == RecoveryStrategy.override) {
_ref.read(profilesProvider.notifier).setProfile(profile); _ref.read(profilesProvider.notifier).value = profiles;
} else {
for (final profile in profiles) {
_ref.read(profilesProvider.notifier).setProfile(
profile,
);
}
} }
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles; final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
if (onlyProfiles) { if (!onlyProfiles) {
final currentProfile = _ref.read(currentProfileProvider); _ref.read(patchClashConfigProvider.notifier).value =
if (currentProfile != null) { config.patchClashConfig;
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id; _ref.read(appSettingProvider.notifier).value = config.appSetting;
} _ref.read(currentProfileIdProvider.notifier).value =
return; 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 ViewMode { mobile, laptop, desktop }
enum LogLevel { debug, info, warning, error, silent } enum LogLevel {
debug,
info,
warning,
error,
silent,
app,
}
enum TransportProtocol { udp, tcp } enum TransportProtocol { udp, tcp }
@@ -249,6 +256,7 @@ enum ActionMethod {
asyncTestDelay, asyncTestDelay,
getConnections, getConnections,
closeConnections, closeConnections,
resetConnections,
closeConnection, closeConnection,
getExternalProviders, getExternalProviders,
getExternalProvider, getExternalProvider,
@@ -262,6 +270,7 @@ enum ActionMethod {
getCountryCode, getCountryCode,
getMemory, getMemory,
getProfile, getProfile,
crash,
///Android, ///Android,
setFdMap, setFdMap,
@@ -283,8 +292,9 @@ enum WindowsHelperServiceStatus {
running, running,
} }
enum DebounceTag { enum FunctionTag {
updateClashConfig, updateClashConfig,
updateStatus,
updateGroups, updateGroups,
addCheckIpNum, addCheckIpNum,
applyProfile, applyProfile,
@@ -299,6 +309,8 @@ enum DebounceTag {
updatePageIndex, updatePageIndex,
pageChange, pageChange,
proxiesTabChange, proxiesTabChange,
logs,
requests,
} }
enum DashboardWidget { enum DashboardWidget {
@@ -308,6 +320,12 @@ enum DashboardWidget {
child: NetworkSpeed(), child: NetworkSpeed(),
), ),
), ),
outboundModeV2(
GridItem(
crossAxisCellCount: 8,
child: OutboundModeV2(),
),
),
outboundMode( outboundMode(
GridItem( GridItem(
crossAxisCellCount: 4, crossAxisCellCount: 4,
@@ -333,6 +351,15 @@ enum DashboardWidget {
), ),
platforms: desktopPlatforms, platforms: desktopPlatforms,
), ),
vpnButton(
GridItem(
crossAxisCellCount: 4,
child: VpnButton(),
),
platforms: [
SupportPlatform.Android,
],
),
systemProxyButton( systemProxyButton(
GridItem( GridItem(
crossAxisCellCount: 4, crossAxisCellCount: 4,
@@ -447,3 +474,14 @@ enum RuleTarget {
DIRECT, DIRECT,
REJECT, 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/common/common.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@immutable @immutable
class Contributor { class Contributor {
@@ -43,6 +47,15 @@ class AboutFragment extends StatelessWidget {
_checkUpdate(context); _checkUpdate(context);
}, },
), ),
ListItem(
title: Text(appLocalizations.contactMe),
onTap: () {
globalState.showMessage(
title: appLocalizations.contactMe,
message: TextSpan(text: "chen08209@gmail.com"),
);
},
),
ListItem( ListItem(
title: const Text("Telegram"), title: const Text("Telegram"),
onTap: () { onTap: () {
@@ -116,33 +129,43 @@ class AboutFragment extends StatelessWidget {
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Wrap( Consumer(builder: (_, ref, ___) {
spacing: 16, return _DeveloperModeDetector(
crossAxisAlignment: WrapCrossAlignment.center, child: Wrap(
children: [ spacing: 16,
Padding( crossAxisAlignment: WrapCrossAlignment.center,
padding: const EdgeInsets.all(12),
child: Image.asset(
'assets/images/icon.png',
width: 64,
height: 64,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Padding(
appName, padding: const EdgeInsets.all(12),
style: Theme.of(context).textTheme.headlineSmall, child: Image.asset(
'assets/images/icon.png',
width: 64,
height: 64,
),
), ),
Text( Column(
globalState.packageInfo.version, crossAxisAlignment: CrossAxisAlignment.start,
style: Theme.of(context).textTheme.labelLarge, 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( const SizedBox(
height: 24, height: 24,
), ),
@@ -179,10 +202,7 @@ class Avatar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return GestureDetector(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
child: Column( child: Column(
children: [ children: [
SizedBox( 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 'dart:convert';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
@@ -22,21 +23,14 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
List<String> acceptList = []; List<String> acceptList = [];
List<String> rejectList = []; List<String> rejectList = [];
late ScrollController _controller; late ScrollController _controller;
final _completer = Completer();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateInitList(); _updateInitList();
_controller = ScrollController(); _controller = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) { _completer.complete(globalState.appController.getPackages());
final appState = globalState.appState;
if (appState.packages.isEmpty) {
Future.delayed(const Duration(milliseconds: 300), () async {
ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
});
}
});
} }
@override @override
@@ -113,15 +107,11 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
} }
_intelligentSelected() async { _intelligentSelected() async {
final appState = globalState.appState; final packageNames = ref.read(
final config = globalState.config; packageListSelectorStateProvider.select(
final accessControl = config.vpnProps.accessControl; (state) => state.list.map((item) => item.packageName),
final packageNames = appState.packages ),
.where( );
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return; if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames = final selectedPackageNames =
@@ -194,7 +184,7 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
final state = ref.watch(packageListSelectorStateProvider); final state = ref.watch(packageListSelectorStateProvider);
final accessControl = state.accessControl; final accessControl = state.accessControl;
final accessControlMode = accessControl.mode; final accessControlMode = accessControl.mode;
final packages = state.getList( final packages = state.getSortList(
accessControlMode == AccessControlMode.acceptSelected accessControlMode == AccessControlMode.acceptSelected
? acceptList ? acceptList
: rejectList, : rejectList,
@@ -323,31 +313,42 @@ class _AccessFragmentState extends ConsumerState<AccessFragment> {
), ),
Expanded( Expanded(
flex: 1, flex: 1,
child: packages.isEmpty child: FutureBuilder(
? const Center( future: _completer.future,
child: CircularProgressIndicator(), builder: (_, snapshot) {
) if (snapshot.connectionState != ConnectionState.done) {
: CommonScrollBar( return Center(
controller: _controller, child: CircularProgressIndicator(),
child: ListView.builder( );
controller: _controller, }
itemCount: packages.length, return packages.isEmpty
itemExtent: 72, ? NullStatus(
itemBuilder: (_, index) { label: appLocalizations.noData,
final package = packages[index]; )
return PackageListItem( : CommonScrollBar(
key: Key(package.packageName), controller: _controller,
package: package, child: ListView.builder(
value: valueList.contains(package.packageName), controller: _controller,
isActive: accessControl.enable, itemCount: packages.length,
onChanged: (value) { itemExtent: 72,
_handleSelected(valueList, package, value); 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(); final lowQuery = query.toLowerCase();
return Consumer( return Consumer(
builder: (context, ref, __) { builder: (context, ref, __) {
final state = ref.watch(packageListSelectorStateProvider); final vm3 = ref.watch(
final accessControl = state.accessControl; packageListSelectorStateProvider.select(
final accessControlMode = accessControl.mode; (state) => VM3(
final packages = state.getList( a: state.getSortList(
accessControlMode == AccessControlMode.acceptSelected state.accessControl.mode == AccessControlMode.acceptSelected
? acceptList ? acceptList
: rejectList, : rejectList,
),
b: state.accessControl.enable,
c: state.accessControl.currentList,
),
),
); );
final packages = vm3.a;
final queryPackages = packages final queryPackages = packages
.where( .where(
(package) => (package) =>
@@ -497,8 +504,8 @@ class AccessControlSearchDelegate extends SearchDelegate {
package.packageName.contains(lowQuery), package.packageName.contains(lowQuery),
) )
.toList(); .toList();
final isAccessControl = state.accessControl.enable; final isAccessControl = vm3.b;
final currentList = accessControl.currentList; final currentList = vm3.c;
final packageNameList = packages.map((e) => e.packageName).toList(); final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList); final valueList = currentList.intersection(packageNameList);
return DisabledMask( 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() { List<Widget> _buildModeSetting() {
return generateSection( return generateSection(
title: appLocalizations.mode, title: appLocalizations.mode,
@@ -673,25 +673,39 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Consumer( child: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final isFilterSystemApp = ref.watch( final vm2 = ref.watch(
vpnSettingProvider vpnSettingProvider.select(
.select((state) => state.accessControl.isFilterSystemApp), (state) => VM2(
a: state.accessControl.isFilterSystemApp,
b: state.accessControl.isFilterNonInternetApp,
),
),
); );
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
for (final item in [false, true]) SettingTextCard(
SettingTextCard( appLocalizations.systemApp,
_getTextWithIsFilterSystemApp(item), isSelected: vm2.a == false,
isSelected: isFilterSystemApp == item, onPressed: () {
onPressed: () { ref.read(vpnSettingProvider.notifier).updateState(
ref.read(vpnSettingProvider.notifier).updateState( (state) => state.copyWith.accessControl(
(state) => state.copyWith.accessControl( isFilterSystemApp: !vm2.a,
isFilterSystemApp: item, ),
), );
); },
}, ),
) 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/state.dart';
import 'package:fl_clash/widgets/dialog.dart'; import 'package:fl_clash/widgets/dialog.dart';
import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/input.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart'; import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
class BackupAndRecovery extends ConsumerWidget { class BackupAndRecovery extends ConsumerWidget {
const BackupAndRecovery({super.key}); const BackupAndRecovery({super.key});
@@ -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 @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final dav = ref.watch(appDAVSettingProvider); final dav = ref.watch(appDAVSettingProvider);
@@ -176,25 +202,25 @@ class BackupAndRecovery extends ConsumerWidget {
builder: (_, snapshot) { builder: (_, snapshot) {
return Center( return Center(
child: FadeThroughBox( child: FadeThroughBox(
child: snapshot.connectionState == child:
ConnectionState.waiting snapshot.connectionState != ConnectionState.done
? const SizedBox( ? const SizedBox(
width: 12, width: 12,
height: 12, height: 12,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 1, strokeWidth: 1,
), ),
) )
: Container( : Container(
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: snapshot.data == true color: snapshot.data == true
? Colors.green ? Colors.green
: Colors.red, : Colors.red,
), ),
width: 12, width: 12,
height: 12, height: 12,
), ),
), ),
); );
}, },
@@ -256,6 +282,26 @@ class BackupAndRecovery extends ConsumerWidget {
title: Text(appLocalizations.recovery), title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc), 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

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

View File

@@ -78,113 +78,72 @@ class ConnectionItem extends ConsumerWidget {
); );
return CommonPopupBox( return CommonPopupBox(
targetBuilder: (open) { targetBuilder: (open) {
return ListItem( openPopup(Offset offset) {
padding: const EdgeInsets.symmetric( open(
horizontal: 16, offset: offset.translate(
vertical: 4, 0,
), 0,
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: value
? GestureDetector(
onTap: () {
if (onClickKeyword == null) return;
final process = connection.metadata.process;
if (process.isEmpty) return;
onClickKeyword!(process);
},
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
), ),
) );
: null, }
title: title,
subtitle: subTitle, return InkWell(
trailing: trailing, child: GestureDetector(
onLongPressStart: (details) {
if (!system.isDesktop) {
return;
}
openPopup(details.localPosition);
},
onSecondaryTapDown: (details) {
if (!system.isDesktop) {
return;
}
openPopup(details.localPosition);
},
child: ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: value
? GestureDetector(
onTap: () {
if (onClickKeyword == null) return;
final process = connection.metadata.process;
if (process.isEmpty) return;
onClickKeyword!(process);
},
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
)
: null,
title: title,
subtitle: subTitle,
trailing: trailing,
),
),
onTap: () {},
); );
// return InkWell(
// child: GestureDetector(
// onLongPressStart: (details) {
// if (!system.isDesktop) {
// return;
// }
// open(
// offset: details.localPosition.translate(
// 0,
// -12,
// ),
// );
// },
// onSecondaryTapDown: (details) {
// if (!system.isDesktop) {
// return;
// }
// open(
// offset: details.localPosition.translate(
// 0,
// -12,
// ),
// );
// },
// child: ListItem(
// padding: const EdgeInsets.symmetric(
// horizontal: 16,
// vertical: 4,
// ),
// tileTitleAlignment: ListTileTitleAlignment.titleHeight,
// leading: value
// ? GestureDetector(
// onTap: () {
// if (onClickKeyword == null) return;
// final process = connection.metadata.process;
// if (process.isEmpty) return;
// onClickKeyword!(process);
// },
// child: Container(
// margin: const EdgeInsets.only(top: 4),
// width: 48,
// height: 48,
// child: FutureBuilder<ImageProvider?>(
// future: _getPackageIcon(connection),
// builder: (_, snapshot) {
// if (!snapshot.hasData && snapshot.data == null) {
// return Container();
// } else {
// return Image(
// image: snapshot.data!,
// gaplessPlayback: true,
// width: 48,
// height: 48,
// );
// }
// },
// ),
// ),
// )
// : null,
// title: title,
// subtitle: subTitle,
// trailing: trailing,
// ),
// ),
// onTap: () {},
// );
}, },
popup: CommonPopupMenu( popup: CommonPopupMenu(
minWidth: 160, minWidth: 160,

View File

@@ -20,12 +20,13 @@ class RequestsFragment extends ConsumerStatefulWidget {
class _RequestsFragmentState extends ConsumerState<RequestsFragment> class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin { with PageMixin {
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey(); final _requestsStateNotifier = ValueNotifier<ConnectionsState>(
final _requestsStateNotifier = const ConnectionsState(loading: true),
ValueNotifier<ConnectionsState>(const ConnectionsState()); );
List<Connection> _requests = []; List<Connection> _requests = [];
final _cacheKey = ValueKey("requests_list"); final _tag = CacheTag.requests;
late ScrollController _scrollController; late ScrollController _scrollController;
bool _isLoad = false;
double _currentMaxWidth = 0; double _currentMaxWidth = 0;
@@ -45,12 +46,13 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1; final preOffset = globalState.cacheScrollPosition[_tag] ?? -1;
_scrollController = ScrollController( _scrollController = ScrollController(
initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite, initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite,
); );
_requests = globalState.appState.requests.list;
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith( _requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: globalState.appState.requests.list, connections: _requests,
); );
ref.listenManual( ref.listenManual(
isCurrentPageProvider( isCurrentPageProvider(
@@ -73,7 +75,6 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
updateRequestsThrottler(); updateRequestsThrottler();
} }
}, },
fireImmediately: true,
); );
} }
@@ -98,14 +99,7 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
final lines = (chainSize.height / baseHeight).round(); final lines = (chainSize.height / baseHeight).round();
final computerHeight = final computerHeight =
size.height + chainSize.height + 24 + 24 * (lines - 1); size.height + chainSize.height + 24 + 24 * (lines - 1);
return computerHeight; return computerHeight + 8 + 32 + globalState.measure.bodyMediumHeight;
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_key.currentState?.clearCache();
}
} }
@override @override
@@ -117,7 +111,7 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
} }
updateRequestsThrottler() { updateRequestsThrottler() {
throttler.call("request", () { throttler.call(FunctionTag.requests, () {
final isEquality = connectionListEquality.equals( final isEquality = connectionListEquality.equals(
_requests, _requests,
_requestsStateNotifier.value.connections, _requestsStateNotifier.value.connections,
@@ -126,13 +120,51 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
return; return;
} }
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith( if(mounted){
connections: _requests, _requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
); connections: _requests,
);
}
}); });
}, duration: commonDuration); }, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
@@ -146,75 +178,86 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
Platform.isAndroid, Platform.isAndroid,
), ),
); );
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0)); _currentMaxWidth = constraints.maxWidth - 40 - (value ? 60 : 0);
return child!; return child!;
}, },
child: ValueListenableBuilder<ConnectionsState>( child: TextScaleNotification(
valueListenable: _requestsStateNotifier, child: ValueListenableBuilder<ConnectionsState>(
builder: (_, state, __) { valueListenable: _requestsStateNotifier,
final connections = state.list; builder: (_, state, __) {
if (connections.isEmpty) { _preLoad();
return NullStatus( final connections = state.list;
label: appLocalizations.nullRequestsDesc, 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>( onNotification: (_) {
(connection) => ConnectionItem( globalState.cacheHeightMap[_tag]?.clear();
key: Key(connection.id),
connection: connection,
onClickKeyword: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: ScrollToEndBox(
controller: _scrollController,
cacheKey: _cacheKey,
dataSource: connections,
child: CommonScrollBar(
controller: _scrollController,
child: CacheItemExtentListView(
key: _key,
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
keyBuilder: (int index) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return "divider";
}
final connection = connections[(index / 2).floor()];
return connection.id;
},
),
),
),
);
}, },
), ),
); );

View File

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

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart'; import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -57,37 +58,43 @@ class _MemoryInfoState extends State<MemoryInfo> {
onPressed: () { onPressed: () {
clashCore.requestGc(); clashCore.requestGc();
}, },
child: Column( child: Container(
children: [ padding: baseInfoEdgeInsets.copyWith(
ValueListenableBuilder( top: 0,
valueListenable: _memoryInfoStateNotifier, ),
builder: (_, trafficValue, __) { child: Column(
return Padding( mainAxisSize: MainAxisSize.max,
padding: baseInfoEdgeInsets.copyWith( mainAxisAlignment: MainAxisAlignment.end,
bottom: 0, crossAxisAlignment: CrossAxisAlignment.start,
top: 12, children: [
), SizedBox(
child: Row( height: globalState.measure.bodyMediumHeight + 2,
children: [ child: ValueListenableBuilder(
Text( valueListenable: _memoryInfoStateNotifier,
trafficValue.showValue, builder: (_, trafficValue, __) {
style: return Row(
context.textTheme.bodyMedium?.toLight.adjustSize(1), mainAxisAlignment: MainAxisAlignment.start,
), children: [
SizedBox( Text(
width: 8, trafficValue.showValue,
), style: context.textTheme.bodyMedium?.toLight
Text( .adjustSize(1),
trafficValue.showUnit, ),
style: SizedBox(
context.textTheme.bodyMedium?.toLight.adjustSize(1), 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/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: false,
isLoading: true,
ipInfo: null,
),
);
class NetworkDetection extends ConsumerStatefulWidget { class NetworkDetection extends ConsumerStatefulWidget {
const NetworkDetection({super.key}); const NetworkDetection({super.key});
@@ -26,101 +14,6 @@ class NetworkDetection extends ConsumerStatefulWidget {
} }
class _NetworkDetectionState extends ConsumerState<NetworkDetection> { 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) { _countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase(); final String code = countryCode.toUpperCase();
if (code.length != 2) { if (code.length != 2) {
@@ -136,7 +29,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
return SizedBox( return SizedBox(
height: getWidgetHeight(1), height: getWidgetHeight(1),
child: ValueListenableBuilder<NetworkDetectionState>( child: ValueListenableBuilder<NetworkDetectionState>(
valueListenable: _networkDetectionState, valueListenable: detectionState.state,
builder: (_, state, __) { builder: (_, state, __) {
final ipInfo = state.ipInfo; final ipInfo = state.ipInfo;
final isLoading = state.isLoading; final isLoading = state.isLoading;
@@ -206,7 +99,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
); );
}, },
icon: Icon( icon: Icon(
size: 16, size: 16.ap,
Icons.info_outline, Icons.info_outline,
color: context.colorScheme.onSurfaceVariant, color: context.colorScheme.onSurfaceVariant,
), ),

View File

@@ -17,58 +17,146 @@ class OutboundMode extends StatelessWidget {
height: height, height: height,
child: Consumer( child: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final mode = final mode = ref.watch(
ref.watch(patchClashConfigProvider.select((state) => state.mode)); patchClashConfigProvider.select(
return CommonCard( (state) => state.mode,
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,
),
),
),
],
),
), ),
); );
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,20 +1,21 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class StartButton extends StatefulWidget { class StartButton extends ConsumerStatefulWidget {
const StartButton({super.key}); const StartButton({super.key});
@override @override
State<StartButton> createState() => _StartButtonState(); ConsumerState<StartButton> createState() => _StartButtonState();
} }
class _StartButtonState extends State<StartButton> class _StartButtonState extends ConsumerState<StartButton>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late AnimationController _controller; late AnimationController _controller;
late Animation<double> _animation;
bool isStart = false; bool isStart = false;
@override @override
@@ -26,6 +27,20 @@ class _StartButtonState extends State<StartButton>
value: isStart ? 1 : 0, value: isStart ? 1 : 0,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
); );
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack,
);
ref.listenManual(
runTimeProvider.select((state) => state != null),
(prev, next) {
if (next != isStart) {
isStart = next;
updateController();
}
},
fireImmediately: true,
);
} }
@override @override
@@ -35,102 +50,98 @@ class _StartButtonState extends State<StartButton>
} }
handleSwitchStart() { handleSwitchStart() {
if (isStart == globalState.appState.isStart) { isStart = !isStart;
isStart = !isStart; updateController();
updateController(); debouncer.call(
globalState.appController.updateStatus(isStart); FunctionTag.updateStatus,
} () {
globalState.appController.updateStatus(isStart);
},
duration: commonDuration,
);
} }
updateController() { updateController() {
if (isStart) { WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.forward(); if (isStart) {
} else { _controller.forward();
_controller.reverse(); } else {
} _controller.reverse();
}
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer( final state = ref.watch(startButtonSelectorStateProvider);
builder: (_, ref, child) { if (!state.isInit || !state.hasProfile) {
final state = ref.watch(startButtonSelectorStateProvider); return Container();
if (!state.isInit || !state.hasProfile) { }
return Container(); return Theme(
} data: Theme.of(context).copyWith(
ref.listenManual( floatingActionButtonTheme: FloatingActionButtonThemeData(
runTimeProvider.select((state) => state != null), sizeConstraints: BoxConstraints(
(prev, next) { minWidth: 56,
if (next != isStart) { maxWidth: 200,
isStart = next; ),
updateController(); ),
} ),
}, child: AnimatedBuilder(
fireImmediately: true, animation: _controller.view,
); builder: (_, child) {
final textWidth = globalState.measure final textWidth = globalState.measure
.computeTextSize( .computeTextSize(
Text( Text(
utils.getTimeDifference( utils.getTimeDifference(
DateTime.now(), DateTime.now(),
),
style: context.textTheme.titleMedium?.toSoftBold,
), ),
style: context.textTheme.titleMedium?.toSoftBold, )
.width +
16;
return FloatingActionButton(
clipBehavior: Clip.antiAlias,
materialTapTargetSize: MaterialTapTargetSize.padded,
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Container(
height: 56,
width: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _animation,
), ),
)
.width +
16;
return AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56 + textWidth * _controller.value,
height: 56,
child: FloatingActionButton(
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
children: [
Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _controller,
),
),
Expanded(
child: ClipRect(
child: OverflowBox(
maxWidth: textWidth,
child: Container(
alignment: Alignment.centerLeft,
child: child!,
),
),
),
),
],
), ),
), SizedBox(
); width: textWidth * _animation.value,
}, child: child!,
child: child, )
); ],
},
child: Consumer(
builder: (_, ref, __) {
final runTime = ref.watch(runTimeProvider);
final text = utils.getTimeText(runTime);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold.copyWith(
color: context.colorScheme.onPrimaryContainer
), ),
); );
}, },
child: Consumer(
builder: (_, ref, __) {
final runTime = ref.watch(runTimeProvider);
final text = utils.getTimeText(runTime);
return Text(
text,
maxLines: 1,
overflow: TextOverflow.visible,
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 'resources.dart';
export 'connection/requests.dart'; export 'connection/requests.dart';
export 'connection/connections.dart'; export 'connection/connections.dart';
export 'developer.dart';

View File

@@ -103,7 +103,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
modifiers: modifiers, modifiers: modifiers,
key: key.usbHidUsage, key: key.usbHidUsage,
); );
return true; return false;
} }
@override @override
@@ -157,59 +157,65 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CommonDialog( return Focus(
title: IntlExt.actionMessage(widget.hotKeyAction.action.name), onKeyEvent: (_, __) {
actions: [ return KeyEventResult.handled;
TextButton( },
onPressed: () { autofocus: true,
_handleRemove(); child: CommonDialog(
}, title: IntlExt.actionMessage(widget.hotKeyAction.action.name),
child: Text(appLocalizations.remove), actions: [
), TextButton(
const SizedBox( onPressed: () {
width: 8, _handleRemove();
), },
TextButton( child: Text(appLocalizations.remove),
onPressed: () {
_handleConfirm();
},
child: Text(
appLocalizations.confirm,
), ),
), const SizedBox(
], width: 8,
child: ValueListenableBuilder( ),
valueListenable: hotKeyActionNotifier, TextButton(
builder: (_, hotKeyAction, ___) { onPressed: () {
final key = hotKeyAction.key; _handleConfirm();
final modifiers = hotKeyAction.modifiers; },
return SizedBox( child: Text(
width: dialogCommonWidth, appLocalizations.confirm,
child: key != null ),
? Wrap( ),
spacing: 8, ],
crossAxisAlignment: WrapCrossAlignment.center, child: ValueListenableBuilder(
children: [ valueListenable: hotKeyActionNotifier,
for (final modifier in modifiers) 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( KeyboardKeyBox(
keyboardKey: modifier.physicalKeys.first, keyboardKey: PhysicalKeyboardKey(key),
), ),
if (modifiers.isNotEmpty) ],
Text( )
"+", : Text(
style: context.textTheme.titleMedium, appLocalizations.pressKeyboard,
), style: context.textTheme.titleMedium,
KeyboardKeyBox( ),
keyboardKey: PhysicalKeyboardKey(key), );
), },
], ),
)
: 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/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
@@ -16,36 +18,27 @@ class LogsFragment extends ConsumerStatefulWidget {
} }
class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin { class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState()); final _logsStateNotifier = ValueNotifier<LogsState>(
final _cacheKey = ValueKey("logs_list"); LogsState(loading: true),
);
late ScrollController _scrollController; late ScrollController _scrollController;
double _currentMaxWidth = 0; double _currentMaxWidth = 0;
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey(); final _tag = CacheTag.rules;
bool _isLoad = false;
List<Log> _logs = []; List<Log> _logs = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1; final position = globalState.cacheScrollPosition[_tag] ?? -1;
_scrollController = ScrollController( _scrollController = ScrollController(
initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite, initialScrollOffset: position > 0 ? position : double.maxFinite,
); );
_logs = globalState.appState.logs.list;
_logsStateNotifier.value = _logsStateNotifier.value.copyWith( _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: globalState.appState.logs.list, logs: _logs,
);
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,
); );
ref.listenManual( ref.listenManual(
isCurrentPageProvider( isCurrentPageProvider(
@@ -60,6 +53,18 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
}, },
fireImmediately: true, 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 @override
@@ -94,13 +99,6 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
super.dispose(); super.dispose();
} }
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_key.currentState?.clearCache();
}
}
_handleExport() async { _handleExport() async {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>( final res = await commonScaffoldState?.loadingRun<bool>(
@@ -123,17 +121,17 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final height = globalState.measure final height = globalState.measure
.computeTextSize( .computeTextSize(
Text( Text(
log.payload ?? "", log.payload,
style: globalState.appController.context.textTheme.bodyLarge, style: context.textTheme.bodyLarge,
), ),
maxWidth: _currentMaxWidth, maxWidth: _currentMaxWidth,
) )
.height; .height;
return height + bodySmallHeight + 8 + bodyMediumHeight + 40; return height + bodySmallHeight + 8 + bodyMediumHeight + 40 + 8;
} }
updateLogsThrottler() { updateLogsThrottler() {
throttler.call("logs", () { throttler.call(FunctionTag.logs, () {
final isEquality = logListEquality.equals( final isEquality = logListEquality.equals(
_logs, _logs,
_logsStateNotifier.value.logs, _logsStateNotifier.value.logs,
@@ -142,82 +140,123 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
return; return;
} }
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith( if (mounted) {
logs: _logs, _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
); logs: _logs,
);
}
}); });
}, duration: commonDuration); }, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (_, constraints) { builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 40); _currentMaxWidth = constraints.maxWidth - 40;
return Align( return ValueListenableBuilder<LogsState>(
alignment: Alignment.topCenter, valueListenable: _logsStateNotifier,
child: ValueListenableBuilder<LogsState>( builder: (_, state, __) {
valueListenable: _logsStateNotifier, _preLoad();
builder: (_, state, __) { final logs = state.list;
final logs = state.list; final items = logs
if (logs.isEmpty) { .map<Widget>(
return NullStatus( (log) => LogItem(
label: appLocalizations.nullLogsDesc, key: Key(log.dateTime),
); log: log,
} onClick: (value) {
final items = logs context.commonScaffoldState?.addKeyword(value);
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return ScrollToEndBox<Log>(
controller: _scrollController,
cacheKey: _cacheKey,
dataSource: logs,
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 ?? "";
}, },
), ),
), )
); .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, vertical: 4,
), ),
title: SelectableText( title: SelectableText(
log.payload ?? '', log.payload,
style: context.textTheme.bodyLarge, style: context.textTheme.bodyLarge,
), ),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SelectableText( SelectableText(
"${log.dateTime}", log.dateTime,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.primary, color: context.colorScheme.primary,
), ),

View File

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

View File

@@ -47,7 +47,6 @@ class _EditProfileState extends State<EditProfile> {
text: widget.profile.autoUpdateDuration.inMinutes.toString(), text: widget.profile.autoUpdateDuration.inMinutes.toString(),
); );
appPath.getProfilePath(widget.profile.id).then((path) async { appPath.getProfilePath(widget.profile.id).then((path) async {
if (path == null) return;
fileInfoNotifier.value = await _getFileInfo(path); fileInfoNotifier.value = await _getFileInfo(path);
}); });
} }
@@ -143,9 +142,10 @@ class _EditProfileState extends State<EditProfile> {
_editProfileFile() async { _editProfileFile() async {
if (rawText == null) { if (rawText == null) {
final profilePath = await appPath.getProfilePath(widget.profile.id); final profilePath = await appPath.getProfilePath(widget.profile.id);
if (profilePath == null) return;
final file = File(profilePath); final file = File(profilePath);
rawText = await file.readAsString(); if (await file.exists()) {
rawText = await file.readAsString();
}
} }
if (!mounted) return; if (!mounted) return;
final title = widget.profile.label ?? widget.profile.id; final title = widget.profile.label ?? widget.profile.id;
@@ -214,6 +214,7 @@ class _EditProfileState extends State<EditProfile> {
final items = [ final items = [
ListItem( ListItem(
title: TextFormField( title: TextFormField(
textInputAction: TextInputAction.next,
controller: labelController, controller: labelController,
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
@@ -230,6 +231,8 @@ class _EditProfileState extends State<EditProfile> {
if (widget.profile.type == ProfileType.url) ...[ if (widget.profile.type == ProfileType.url) ...[
ListItem( ListItem(
title: TextFormField( title: TextFormField(
textInputAction: TextInputAction.next,
keyboardType: TextInputType.url,
controller: urlController, controller: urlController,
maxLines: 5, maxLines: 5,
minLines: 1, minLines: 1,
@@ -258,6 +261,7 @@ class _EditProfileState extends State<EditProfile> {
if (autoUpdate) if (autoUpdate)
ListItem( ListItem(
title: TextFormField( title: TextFormField(
textInputAction: TextInputAction.next,
controller: autoUpdateDurationController, controller: autoUpdateDurationController,
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),

View File

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

View File

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

View File

@@ -133,7 +133,6 @@ class ProxyCard extends StatelessWidget {
ref.watch(getSelectedProxyNameProvider(groupName)); ref.watch(getSelectedProxyNameProvider(groupName));
return CommonCard( return CommonCard(
key: key, key: key,
enterAnimated: true,
onPressed: () { onPressed: () {
_changeProxy(ref); _changeProxy(ref);
}, },
@@ -177,8 +176,8 @@ class ProxyCard extends StatelessWidget {
proxy.type, proxy.type,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: color: context
context.textTheme.bodySmall?.color?.opacity80, .textTheme.bodySmall?.color?.opacity80,
), ),
), ),
), ),

View File

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

View File

@@ -114,12 +114,16 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
required int columns, required int columns,
required Set<String> currentUnfoldSet, required Set<String> currentUnfoldSet,
required ProxyCardType type, required ProxyCardType type,
required String query,
}) { }) {
final items = <Widget>[]; final items = <Widget>[];
final GroupNameProxiesMap groupNameProxiesMap = {}; final GroupNameProxiesMap groupNameProxiesMap = {};
for (final groupName in groupNames) { for (final groupName in groupNames) {
final group = final group = ref.read(
ref.read(groupsProvider.select((state) => state.getGroup(groupName))); groupsProvider.select(
(state) => state.getGroup(groupName),
),
);
if (group == null) { if (group == null) {
continue; continue;
} }
@@ -140,7 +144,9 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
]); ]);
if (isExpand) { if (isExpand) {
final sortedProxies = globalState.appController.getSortProxies( final sortedProxies = globalState.appController.getSortProxies(
group.all, group.all
.where((item) => item.name.toLowerCase().contains(query))
.toList(),
group.testUrl, group.testUrl,
); );
groupNameProxiesMap[groupName] = sortedProxies; groupNameProxiesMap[groupName] = sortedProxies;
@@ -250,6 +256,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return Consumer( return Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final state = ref.watch(proxiesListSelectorStateProvider); final state = ref.watch(proxiesListSelectorStateProvider);
ref.watch(themeSettingProvider.select((state) => state.textScale));
if (state.groupNames.isEmpty) { if (state.groupNames.isEmpty) {
return NullStatus( return NullStatus(
label: appLocalizations.nullProxies, label: appLocalizations.nullProxies,
@@ -261,6 +268,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
currentUnfoldSet: state.currentUnfoldSet, currentUnfoldSet: state.currentUnfoldSet,
columns: state.columns, columns: state.columns,
type: state.proxyCardType, type: state.proxyCardType,
query: state.query,
); );
final itemsOffset = _getItemHeightList(items, state.proxyCardType); final itemsOffset = _getItemHeightList(items, state.proxyCardType);
return CommonScrollBar( return CommonScrollBar(
@@ -414,7 +422,10 @@ class _ListHeaderState extends State<ListHeader>
return Consumer( return Consumer(
builder: (_, ref, child) { builder: (_, ref, child) {
final iconStyle = ref.watch( final iconStyle = ref.watch(
proxiesStyleSettingProvider.select((state) => state.iconStyle)); proxiesStyleSettingProvider.select(
(state) => state.iconStyle,
),
);
final icon = ref.watch(proxiesStyleSettingProvider.select((state) { final icon = ref.watch(proxiesStyleSettingProvider.select((state) {
final iconMapEntryList = state.iconMap.entries.toList(); final iconMapEntryList = state.iconMap.entries.toList();
final index = iconMapEntryList.indexWhere((item) { final index = iconMapEntryList.indexWhere((item) {
@@ -430,30 +441,44 @@ class _ListHeaderState extends State<ListHeader>
return this.icon; return this.icon;
})); }));
return switch (iconStyle) { return switch (iconStyle) {
ProxiesIconStyle.standard => Container( ProxiesIconStyle.standard => LayoutBuilder(
height: 48, builder: (_, constraints) {
width: 48, return Container(
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
right: 16, right: 16,
), ),
padding: const EdgeInsets.all(8), child: AspectRatio(
decoration: BoxDecoration( aspectRatio: 1,
color: context.colorScheme.secondaryContainer, child: Container(
borderRadius: BorderRadius.circular(12), height: constraints.maxHeight,
), width: constraints.maxWidth,
clipBehavior: Clip.antiAlias, alignment: Alignment.center,
child: CommonTargetIcon( padding: EdgeInsets.all(6.ap),
src: icon, decoration: BoxDecoration(
size: 32, color: context.colorScheme.secondaryContainer,
), borderRadius: BorderRadius.circular(12),
),
clipBehavior: Clip.antiAlias,
child: CommonTargetIcon(
src: icon,
size: constraints.maxHeight - 12.ap,
),
),
),
);
},
), ),
ProxiesIconStyle.icon => Container( ProxiesIconStyle.icon => Container(
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
right: 16, right: 16,
), ),
child: CommonTargetIcon( child: LayoutBuilder(
src: icon, builder: (_, constraints) {
size: 42, return CommonTargetIcon(
src: icon,
size: constraints.maxHeight - 8,
);
},
), ),
), ),
ProxiesIconStyle.none => Container(), ProxiesIconStyle.none => Container(),
@@ -467,7 +492,7 @@ class _ListHeaderState extends State<ListHeader>
return CommonCard( return CommonCard(
enterAnimated: widget.enterAnimated, enterAnimated: widget.enterAnimated,
key: widget.key, key: widget.key,
radius: 14, radius: 16.ap,
type: CommonCardType.filled, type: CommonCardType.filled,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -570,7 +595,10 @@ class _ListHeaderState extends State<ListHeader>
const SizedBox( const SizedBox(
width: 6, width: 6,
), ),
], ] else
SizedBox(
width: 4,
),
AnimatedBuilder( AnimatedBuilder(
animation: _animationController.view, animation: _animationController.view,
builder: (_, __) { builder: (_, __) {

View File

@@ -2,10 +2,12 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/list.dart'; import 'package:fl_clash/fragments/proxies/list.dart';
import 'package:fl_clash/fragments/proxies/providers.dart'; import 'package:fl_clash/fragments/proxies/providers.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'common.dart'; import 'common.dart';
import 'setting.dart'; import 'setting.dart';
import 'tab.dart'; import 'tab.dart';
@@ -25,70 +27,97 @@ class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
@override @override
get actions => [ get actions => [
if (_hasProviders) CommonPopupBox(
IconButton( targetBuilder: (open) {
onPressed: () { return IconButton(
showExtend( onPressed: () {
context, open(
builder: (_, type) { offset: Offset(0, 20),
return ProvidersView( );
type: type, },
); icon: Icon(
}, Icons.more_vert,
); ),
}, );
icon: const Icon( },
Icons.poll_outlined, popup: CommonPopupMenu(
), items: [
), PopupMenuItemData(
_isTab icon: Icons.tune,
? IconButton( label: appLocalizations.settings,
onPressed: () { onPressed: () {
_proxiesTabKey.currentState?.scrollToGroupSelected(); showSheet(
}, context: context,
icon: const Icon( props: SheetProps(
Icons.adjust_outlined, isScrollControlled: true,
), ),
)
: IconButton(
onPressed: () {
showExtend(
context,
builder: (_, type) { builder: (_, type) {
return AdaptiveSheetScaffold( return AdaptiveSheetScaffold(
type: type, type: type,
body: const _IconConfigView(), body: const ProxiesSetting(),
title: appLocalizations.iconConfiguration, title: appLocalizations.settings,
); );
}, },
); );
}, },
icon: const Icon( ),
Icons.style_outlined, if (_hasProviders)
PopupMenuItemData(
icon: Icons.poll_outlined,
label: appLocalizations.providers,
onPressed: () {
showExtend(
context,
builder: (_, type) {
return ProvidersView(
type: type,
);
},
);
},
), ),
), _isTab
IconButton( ? PopupMenuItemData(
onPressed: () { icon: Icons.adjust_outlined,
showSheet( label: "聚焦",
context: context, onPressed: () {
props: SheetProps( _proxiesTabKey.currentState?.scrollToGroupSelected();
isScrollControlled: true, },
), )
builder: (_, type) { : PopupMenuItemData(
return AdaptiveSheetScaffold( icon: Icons.style_outlined,
type: type, label: appLocalizations.iconConfiguration,
body: const ProxiesSetting(), onPressed: () {
title: appLocalizations.proxiesSetting, showExtend(
); context,
}, builder: (_, type) {
); return AdaptiveSheetScaffold(
}, type: type,
icon: const Icon( body: const _IconConfigView(),
Icons.tune, title: appLocalizations.iconConfiguration,
);
},
);
},
),
],
), ),
) )
]; ];
@override
get onSearch => (value) {
ref.read(proxiesQueryProvider.notifier).value = value;
};
@override
void dispose() {
super.dispose();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(proxiesQueryProvider.notifier).value = "";
});
}
@override @override
get floatingActionButton => _isTab get floatingActionButton => _isTab
? DelayTestButton( ? DelayTestButton(
@@ -103,6 +132,70 @@ class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
@override @override
void initState() { void initState() {
[
if (_hasProviders)
IconButton(
onPressed: () {
showExtend(
context,
builder: (_, type) {
return ProvidersView(
type: type,
);
},
);
},
icon: const Icon(
Icons.poll_outlined,
),
),
_isTab
? IconButton(
onPressed: () {
_proxiesTabKey.currentState?.scrollToGroupSelected();
},
icon: const Icon(
Icons.adjust_outlined,
),
)
: IconButton(
onPressed: () {
showExtend(
context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: const _IconConfigView(),
title: appLocalizations.iconConfiguration,
);
},
);
},
icon: const Icon(
Icons.style_outlined,
),
),
IconButton(
onPressed: () {
showSheet(
context: context,
props: SheetProps(
isScrollControlled: true,
),
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: const ProxiesSetting(),
title: appLocalizations.settings,
);
},
);
},
icon: const Icon(
Icons.tune,
),
)
];
ref.listenManual( ref.listenManual(
proxiesActionsStateProvider, proxiesActionsStateProvider,
fireImmediately: true, fireImmediately: true,
@@ -123,8 +216,11 @@ class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final proxiesType = final proxiesType = ref.watch(
ref.watch(proxiesStyleSettingProvider.select((state) => state.type)); proxiesStyleSettingProvider.select(
(state) => state.type,
),
);
return switch (proxiesType) { return switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment( ProxiesType.tab => ProxiesTabFragment(
key: _proxiesTabKey, key: _proxiesTabKey,
@@ -139,8 +235,9 @@ class _IconConfigView extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final iconMap = final iconMap = ref.watch(proxiesStyleSettingProvider.select(
ref.watch(proxiesStyleSettingProvider.select((state) => state.iconMap)); (state) => state.iconMap,
));
return MapInputPage( return MapInputPage(
title: appLocalizations.iconConfiguration, title: appLocalizations.iconConfiguration,
map: iconMap, map: iconMap,

View File

@@ -1,13 +1,13 @@
import 'dart:math'; import 'dart:math';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/common.dart';
import 'card.dart'; import 'card.dart';
import 'common.dart'; import 'common.dart';
@@ -127,8 +127,6 @@ class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
return; return;
} }
final currentGroup = currentGroups[groupIndex]; final currentGroup = currentGroups[groupIndex];
currentTabProxies = currentGroup.all;
currentTabTestUrl = currentGroup.testUrl;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateCurrentGroupName( globalState.appController.updateCurrentGroupName(
currentGroup.name, currentGroup.name,
@@ -164,7 +162,7 @@ class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
if (prev == next) { if (prev == next) {
return; return;
} }
if (prev?.groupNames.length != next.groupNames.length) { if (!stringListEquality.equals(prev?.groupNames, next.groupNames)) {
_destroyTabController(); _destroyTabController();
final index = next.groupNames.indexWhere( final index = next.groupNames.indexWhere(
(item) => item == next.currentGroupName, (item) => item == next.currentGroupName,
@@ -178,6 +176,7 @@ class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ref.watch(themeSettingProvider.select((state) => state.textScale));
final state = ref.watch(groupNamesStateProvider); final state = ref.watch(groupNamesStateProvider);
final groupNames = state.groupNames; final groupNames = state.groupNames;
if (groupNames.isEmpty) { if (groupNames.isEmpty) {
@@ -294,17 +293,12 @@ class ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
if (_controller.position.maxScrollExtent == 0) { if (_controller.position.maxScrollExtent == 0) {
return; return;
} }
final sortedProxies = globalState.appController.getSortProxies(
currentTabProxies,
currentTabTestUrl,
);
_controller.animateTo( _controller.animateTo(
min( min(
16 + 16 +
getScrollToSelectedOffset( getScrollToSelectedOffset(
groupName: groupName, groupName: groupName,
proxies: sortedProxies, proxies: currentTabProxies,
), ),
_controller.position.maxScrollExtent, _controller.position.maxScrollExtent,
), ),
@@ -323,6 +317,8 @@ class ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
proxies, proxies,
state.testUrl, state.testUrl,
); );
currentTabProxies = sortedProxies;
currentTabTestUrl = state.testUrl;
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: CommonAutoHiddenScrollBar( child: CommonAutoHiddenScrollBar(

View File

@@ -147,22 +147,21 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
FutureBuilder<FileInfo>( FutureBuilder<FileInfo>(
future: _getGeoFileLastModified(geoItem.fileName), future: _getGeoFileLastModified(geoItem.fileName),
builder: (_, snapshot) { builder: (_, snapshot) {
final height = globalState.measure.bodyMediumHeight;
return SizedBox( return SizedBox(
height: 24, height: height,
child: FadeThroughBox( child: snapshot.data == null
key: Key("fade_box_${geoItem.label}"), ? SizedBox(
child: snapshot.data == null width: height,
? const SizedBox( height: height,
width: 24, child: CircularProgressIndicator(
height: 24, strokeWidth: 2,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
snapshot.data!.desc,
), ),
), )
: Text(
snapshot.data!.desc,
style: context.textTheme.bodyMedium,
),
); );
}, },
), ),

View File

@@ -1,3 +1,5 @@
// ignore_for_file: deprecated_member_use
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
@@ -39,7 +41,20 @@ class ThemeFragment extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView(child: ThemeColorsBox()); return SingleChildScrollView(
child: Column(
spacing: 24,
children: [
_ThemeModeItem(),
_PrimaryColorItem(),
_PrueBlackItem(),
_TextScaleFactorItem(),
const SizedBox(
height: 64,
),
],
),
);
} }
} }
@@ -57,42 +72,14 @@ class ItemCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Wrap(
padding: const EdgeInsets.only( runSpacing: 16,
top: 16,
),
child: Wrap(
runSpacing: 16,
children: [
InfoHeader(
info: info,
actions: actions,
),
child,
],
),
);
}
}
class ThemeColorsBox extends ConsumerStatefulWidget {
const ThemeColorsBox({super.key});
@override
ConsumerState<ThemeColorsBox> createState() => _ThemeColorsBoxState();
}
class _ThemeColorsBoxState extends ConsumerState<ThemeColorsBox> {
@override
Widget build(BuildContext context) {
return Column(
children: [ children: [
_ThemeModeItem(), InfoHeader(
_PrimaryColorItem(), info: info,
_PrueBlackItem(), actions: actions,
const SizedBox(
height: 64,
), ),
child,
], ],
); );
} }
@@ -296,137 +283,153 @@ class _PrimaryColorItemState extends ConsumerState<_PrimaryColorItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final vm3 = ref.watch( final vm4 = ref.watch(
themeSettingProvider.select( themeSettingProvider.select(
(state) => VM3( (state) => VM4(
a: state.primaryColor, a: state.primaryColor,
b: state.primaryColors, b: state.primaryColors,
c: state.schemeVariant, c: state.schemeVariant,
d: state.primaryColor == defaultPrimaryColor &&
intListEquality.equals(state.primaryColors, defaultPrimaryColors),
), ),
), ),
); );
final primaryColor = vm3.a; final primaryColor = vm4.a;
final primaryColors = [null, ...vm3.b]; final primaryColors = [null, ...vm4.b];
final schemeVariant = vm3.c; final schemeVariant = vm4.c;
final isEquals = vm4.d;
return ItemCard( return CommonPopScope(
info: Info( onPop: () {
label: appLocalizations.themeColor, if (_removablePrimaryColor != null) {
iconData: Icons.palette, setState(() {
), _removablePrimaryColor = null;
actions: genActions( });
[ return false;
if (_removablePrimaryColor == null) }
FilledButton( return true;
style: ButtonStyle( },
visualDensity: VisualDensity.compact, child: ItemCard(
), info: Info(
onPressed: _handleChangeSchemeVariant, label: appLocalizations.themeColor,
child: Text(Intl.message("${schemeVariant.name}Scheme")), iconData: Icons.palette,
),
_removablePrimaryColor != null
? FilledButton(
style: ButtonStyle(
visualDensity: VisualDensity.compact,
),
onPressed: () {
setState(() {
_removablePrimaryColor = null;
});
},
child: Text(appLocalizations.cancel),
)
: IconButton.filledTonal(
iconSize: 20,
padding: EdgeInsets.all(4),
visualDensity: VisualDensity.compact,
onPressed: _handleReset,
icon: Icon(Icons.replay),
)
],
space: 8,
),
child: Container(
margin: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
), ),
child: LayoutBuilder( actions: genActions(
builder: (_, constraints) { [
final columns = _calcColumns(constraints.maxWidth); if (_removablePrimaryColor == null)
final itemWidth = FilledButton(
(constraints.maxWidth - (columns - 1) * 16) / columns; style: ButtonStyle(
return Wrap( visualDensity: VisualDensity.compact,
spacing: 16, ),
runSpacing: 16, onPressed: _handleChangeSchemeVariant,
children: [ child: Text(Intl.message("${schemeVariant.name}Scheme")),
for (final color in primaryColors) ),
Container( if (_removablePrimaryColor != null)
clipBehavior: Clip.none, FilledButton(
width: itemWidth, style: ButtonStyle(
height: itemWidth, visualDensity: VisualDensity.compact,
child: Stack( ),
alignment: Alignment.center, onPressed: () {
setState(() {
_removablePrimaryColor = null;
});
},
child: Text(appLocalizations.cancel),
),
if (_removablePrimaryColor == null && !isEquals)
IconButton.filledTonal(
iconSize: 20,
padding: EdgeInsets.all(4),
visualDensity: VisualDensity.compact,
onPressed: _handleReset,
icon: Icon(Icons.replay),
)
],
space: 8,
),
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
),
child: LayoutBuilder(
builder: (_, constraints) {
final columns = _calcColumns(constraints.maxWidth);
final itemWidth =
(constraints.maxWidth - (columns - 1) * 16) / columns;
return Wrap(
spacing: 16,
runSpacing: 16,
children: [
for (final color in primaryColors)
Container(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ width: itemWidth,
EffectGestureDetector( height: itemWidth,
child: ColorSchemeBox( child: Stack(
isSelected: color == primaryColor, alignment: Alignment.center,
primaryColor: color != null ? Color(color) : null, clipBehavior: Clip.none,
onPressed: () { children: [
ref EffectGestureDetector(
.read(themeSettingProvider.notifier) child: ColorSchemeBox(
.updateState( isSelected: color == primaryColor,
(state) => state.copyWith( primaryColor: color != null ? Color(color) : null,
primaryColor: color, onPressed: () {
), setState(() {
); _removablePrimaryColor = null;
});
ref
.read(themeSettingProvider.notifier)
.updateState(
(state) => state.copyWith(
primaryColor: color,
),
);
},
),
onLongPress: () {
setState(() {
_removablePrimaryColor = color;
});
}, },
), ),
onLongPress: () { if (_removablePrimaryColor != null &&
setState(() { _removablePrimaryColor == color)
_removablePrimaryColor = color; Container(
}); color: Colors.white.opacity0,
}, padding: EdgeInsets.all(8),
), child: IconButton.filledTonal(
if (_removablePrimaryColor != null && onPressed: _handleDel,
_removablePrimaryColor == color) padding: EdgeInsets.all(12),
Container( iconSize: 30,
color: Colors.white.opacity0, icon: Icon(
padding: EdgeInsets.all(8), color: context.colorScheme.primary,
child: IconButton.filledTonal( Icons.delete,
onPressed: _handleDel, ),
padding: EdgeInsets.all(12),
iconSize: 30,
icon: Icon(
color: context.colorScheme.primary,
Icons.delete,
), ),
), ),
), ],
],
),
),
if (_removablePrimaryColor == null)
Container(
width: itemWidth,
height: itemWidth,
padding: EdgeInsets.all(
4,
),
child: IconButton.filledTonal(
onPressed: _handleAdd,
iconSize: 32,
icon: Icon(
color: context.colorScheme.primary,
Icons.add,
), ),
), ),
) if (_removablePrimaryColor == null)
], Container(
); width: itemWidth,
}, height: itemWidth,
padding: EdgeInsets.all(
4,
),
child: IconButton.filledTonal(
onPressed: _handleAdd,
iconSize: 32,
icon: Icon(
color: context.colorScheme.primary,
Icons.add,
),
),
)
],
);
},
),
), ),
), ),
); );
@@ -438,33 +441,118 @@ class _PrueBlackItem extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final prueBlack = final prueBlack = ref.watch(
ref.watch(themeSettingProvider.select((state) => state.pureBlack)); themeSettingProvider.select(
return Padding( (state) => state.pureBlack,
padding: const EdgeInsets.symmetric(vertical: 16),
child: ListItem.switchItem(
leading: Icon(
Icons.contrast,
),
horizontalTitleGap: 12,
title: Text(
appLocalizations.pureBlackMode,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
delegate: SwitchDelegate(
value: prueBlack,
onChanged: (value) {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith(
pureBlack: value,
),
);
},
),
), ),
); );
return ListItem.switchItem(
leading: Icon(
Icons.contrast,
),
horizontalTitleGap: 12,
title: Text(
appLocalizations.pureBlackMode,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
delegate: SwitchDelegate(
value: prueBlack,
onChanged: (value) {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith(
pureBlack: value,
),
);
},
),
);
}
}
class _TextScaleFactorItem extends ConsumerWidget {
const _TextScaleFactorItem();
@override
Widget build(BuildContext context, WidgetRef ref) {
final textScale = ref.watch(
themeSettingProvider.select(
(state) => state.textScale,
),
);
final String process = "${((textScale.scale * 100) as double).round()}%";
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(bottom: 8),
child: ListItem.switchItem(
leading: Icon(
Icons.text_fields,
),
horizontalTitleGap: 12,
title: Text(
appLocalizations.textScale,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
delegate: SwitchDelegate(
value: textScale.enable,
onChanged: (value) {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith.textScale(
enable: value,
),
);
},
),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
spacing: 32,
children: [
Expanded(
child: DisabledMask(
status: !textScale.enable,
child: ActivateBox(
active: textScale.enable,
child: SliderTheme(
data: _SliderDefaultsM3(context),
child: Slider(
padding: EdgeInsets.zero,
min: minTextScale,
max: maxTextScale,
value: textScale.scale,
onChanged: (value) {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith.textScale(
scale: value,
),
);
},
),
),
),
),
),
Padding(
padding: EdgeInsets.only(right: 4),
child: Text(
process,
style: context.textTheme.titleMedium,
),
),
],
),
),
],
);
} }
} }
@@ -530,3 +618,112 @@ class _PaletteDialogState extends State<_PaletteDialog> {
); );
} }
} }
class _SliderDefaultsM3 extends SliderThemeData {
_SliderDefaultsM3(this.context) : super(trackHeight: 16.0);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
Color? get activeTrackColor => _colors.primary;
@override
Color? get inactiveTrackColor => _colors.secondaryContainer;
@override
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
@override
Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38);
@override
Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);
@override
Color? get disabledSecondaryActiveTrackColor =>
_colors.onSurface.withOpacity(0.38);
@override
Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(1.0);
@override
Color? get inactiveTickMarkColor =>
_colors.onSecondaryContainer.withOpacity(1.0);
@override
Color? get disabledActiveTickMarkColor => _colors.onInverseSurface;
@override
Color? get disabledInactiveTickMarkColor => _colors.onSurface;
@override
Color? get thumbColor => _colors.primary;
@override
Color? get disabledThumbColor => _colors.onSurface.withOpacity(0.38);
@override
Color? get overlayColor =>
WidgetStateColor.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.dragged)) {
return _colors.primary.withOpacity(0.1);
}
if (states.contains(WidgetState.hovered)) {
return _colors.primary.withOpacity(0.08);
}
if (states.contains(WidgetState.focused)) {
return _colors.primary.withOpacity(0.1);
}
return Colors.transparent;
});
@override
TextStyle? get valueIndicatorTextStyle =>
Theme.of(context).textTheme.labelLarge!.copyWith(
color: _colors.onInverseSurface,
);
@override
Color? get valueIndicatorColor => _colors.inverseSurface;
@override
SliderComponentShape? get valueIndicatorShape =>
const RoundedRectSliderValueIndicatorShape();
@override
SliderComponentShape? get thumbShape => const HandleThumbShape();
@override
SliderTrackShape? get trackShape => const GappedSliderTrackShape();
@override
SliderComponentShape? get overlayShape => const RoundSliderOverlayShape();
@override
SliderTickMarkShape? get tickMarkShape =>
const RoundSliderTickMarkShape(tickMarkRadius: 4.0 / 2);
@override
WidgetStateProperty<Size?>? get thumbSize {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return const Size(4.0, 44.0);
}
if (states.contains(WidgetState.hovered)) {
return const Size(4.0, 44.0);
}
if (states.contains(WidgetState.focused)) {
return const Size(2.0, 44.0);
}
if (states.contains(WidgetState.pressed)) {
return const Size(2.0, 44.0);
}
return const Size(4.0, 44.0);
});
}
@override
double? get trackGap => 6.0;
}

View File

@@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'backup_and_recovery.dart'; import 'backup_and_recovery.dart';
import 'developer.dart';
import 'theme.dart'; import 'theme.dart';
import 'package:path/path.dart' show dirname, join; import 'package:path/path.dart' show dirname, join;
@@ -54,11 +55,12 @@ class _ToolboxFragmentState extends ConsumerState<ToolsFragment> {
); );
} }
List<Widget> _getOtherList() { List<Widget> _getOtherList(bool enableDeveloperMode) {
return generateSection( return generateSection(
title: appLocalizations.other, title: appLocalizations.other,
items: [ items: [
_DisclaimerItem(), _DisclaimerItem(),
if (enableDeveloperMode) _DeveloperItem(),
_InfoItem(), _InfoItem(),
], ],
); );
@@ -82,7 +84,11 @@ class _ToolboxFragmentState extends ConsumerState<ToolsFragment> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ref.watch(appSettingProvider.select((state) => state.locale)); final vm2 = ref.watch(
appSettingProvider.select(
(state) => VM2(a: state.locale, b: state.developerMode),
),
);
final items = [ final items = [
Consumer( Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
@@ -99,7 +105,7 @@ class _ToolboxFragmentState extends ConsumerState<ToolsFragment> {
}, },
), ),
..._getSettingList(), ..._getSettingList(),
..._getOtherList(), ..._getOtherList(vm2.b),
]; ];
return ListView.builder( return ListView.builder(
itemCount: items.length, itemCount: items.length,
@@ -297,3 +303,19 @@ class _InfoItem extends StatelessWidget {
); );
} }
} }
class _DeveloperItem extends StatelessWidget {
const _DeveloperItem();
@override
Widget build(BuildContext context) {
return ListItem.open(
leading: const Icon(Icons.developer_board),
title: Text(appLocalizations.developerMode),
delegate: OpenDelegate(
title: appLocalizations.developerMode,
widget: const DeveloperView(),
),
);
}
}

View File

@@ -146,6 +146,7 @@ class MessageLookup extends MessageLookupByLibrary {
"The current application is already the latest version", "The current application is already the latest version",
), ),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."), "checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"clearData": MessageLookupByLibrary.simpleMessage("Clear Data"),
"clipboardExport": MessageLookupByLibrary.simpleMessage("Export clipboard"), "clipboardExport": MessageLookupByLibrary.simpleMessage("Export clipboard"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("Clipboard import"), "clipboardImport": MessageLookupByLibrary.simpleMessage("Clipboard import"),
"colorExists": MessageLookupByLibrary.simpleMessage( "colorExists": MessageLookupByLibrary.simpleMessage(
@@ -163,6 +164,7 @@ class MessageLookup extends MessageLookupByLibrary {
"View current connections data", "View current connections data",
), ),
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"), "connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"contactMe": MessageLookupByLibrary.simpleMessage("Contact me"),
"content": MessageLookupByLibrary.simpleMessage("Content"), "content": MessageLookupByLibrary.simpleMessage("Content"),
"contentEmptyTip": MessageLookupByLibrary.simpleMessage( "contentEmptyTip": MessageLookupByLibrary.simpleMessage(
"Content cannot be empty", "Content cannot be empty",
@@ -177,6 +179,7 @@ class MessageLookup extends MessageLookupByLibrary {
"core": MessageLookupByLibrary.simpleMessage("Core"), "core": MessageLookupByLibrary.simpleMessage("Core"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"), "coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
"country": MessageLookupByLibrary.simpleMessage("Country"), "country": MessageLookupByLibrary.simpleMessage("Country"),
"crashTest": MessageLookupByLibrary.simpleMessage("Crash test"),
"create": MessageLookupByLibrary.simpleMessage("Create"), "create": MessageLookupByLibrary.simpleMessage("Create"),
"cut": MessageLookupByLibrary.simpleMessage("Cut"), "cut": MessageLookupByLibrary.simpleMessage("Cut"),
"dark": MessageLookupByLibrary.simpleMessage("Dark"), "dark": MessageLookupByLibrary.simpleMessage("Dark"),
@@ -208,6 +211,10 @@ class MessageLookup extends MessageLookupByLibrary {
"detectionTip": MessageLookupByLibrary.simpleMessage( "detectionTip": MessageLookupByLibrary.simpleMessage(
"Relying on third-party api is for reference only", "Relying on third-party api is for reference only",
), ),
"developerMode": MessageLookupByLibrary.simpleMessage("Developer mode"),
"developerModeEnableTip": MessageLookupByLibrary.simpleMessage(
"Developer mode is enabled.",
),
"direct": MessageLookupByLibrary.simpleMessage("Direct"), "direct": MessageLookupByLibrary.simpleMessage("Direct"),
"disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"), "disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "disclaimerDesc": MessageLookupByLibrary.simpleMessage(
@@ -320,6 +327,7 @@ class MessageLookup extends MessageLookupByLibrary {
"intelligentSelected": MessageLookupByLibrary.simpleMessage( "intelligentSelected": MessageLookupByLibrary.simpleMessage(
"Intelligent selection", "Intelligent selection",
), ),
"internet": MessageLookupByLibrary.simpleMessage("Internet"),
"intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"), "intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"), "ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage( "ipv6Desc": MessageLookupByLibrary.simpleMessage(
@@ -356,12 +364,17 @@ class MessageLookup extends MessageLookupByLibrary {
), ),
"logs": MessageLookupByLibrary.simpleMessage("Logs"), "logs": MessageLookupByLibrary.simpleMessage("Logs"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"), "logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"),
"logsTest": MessageLookupByLibrary.simpleMessage("Logs test"),
"loopback": MessageLookupByLibrary.simpleMessage("Loopback unlock tool"), "loopback": MessageLookupByLibrary.simpleMessage("Loopback unlock tool"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage( "loopbackDesc": MessageLookupByLibrary.simpleMessage(
"Used for UWP loopback unlocking", "Used for UWP loopback unlocking",
), ),
"loose": MessageLookupByLibrary.simpleMessage("Loose"), "loose": MessageLookupByLibrary.simpleMessage("Loose"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("Memory info"), "memoryInfo": MessageLookupByLibrary.simpleMessage("Memory info"),
"messageTest": MessageLookupByLibrary.simpleMessage("Message test"),
"messageTestTip": MessageLookupByLibrary.simpleMessage(
"This is a message.",
),
"min": MessageLookupByLibrary.simpleMessage("Min"), "min": MessageLookupByLibrary.simpleMessage("Min"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("Minimize on exit"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("Minimize on exit"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
@@ -399,6 +412,7 @@ class MessageLookup extends MessageLookupByLibrary {
"noInfo": MessageLookupByLibrary.simpleMessage("No info"), "noInfo": MessageLookupByLibrary.simpleMessage("No info"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"),
"noNetwork": MessageLookupByLibrary.simpleMessage("No network"), "noNetwork": MessageLookupByLibrary.simpleMessage("No network"),
"noNetworkApp": MessageLookupByLibrary.simpleMessage("No network APP"),
"noProxy": MessageLookupByLibrary.simpleMessage("No proxy"), "noProxy": MessageLookupByLibrary.simpleMessage("No proxy"),
"noProxyDesc": MessageLookupByLibrary.simpleMessage( "noProxyDesc": MessageLookupByLibrary.simpleMessage(
"Please create a profile or add a valid profile", "Please create a profile or add a valid profile",
@@ -526,6 +540,15 @@ class MessageLookup extends MessageLookupByLibrary {
"recoveryProfiles": MessageLookupByLibrary.simpleMessage( "recoveryProfiles": MessageLookupByLibrary.simpleMessage(
"Only recovery profiles", "Only recovery profiles",
), ),
"recoveryStrategy": MessageLookupByLibrary.simpleMessage(
"Recovery strategy",
),
"recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage(
"Compatible",
),
"recoveryStrategy_override": MessageLookupByLibrary.simpleMessage(
"Override",
),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("Recovery success"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("Recovery success"),
"redo": MessageLookupByLibrary.simpleMessage("redo"), "redo": MessageLookupByLibrary.simpleMessage("redo"),
"regExp": MessageLookupByLibrary.simpleMessage("RegExp"), "regExp": MessageLookupByLibrary.simpleMessage("RegExp"),
@@ -612,6 +635,7 @@ class MessageLookup extends MessageLookupByLibrary {
"submit": MessageLookupByLibrary.simpleMessage("Submit"), "submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"), "sync": MessageLookupByLibrary.simpleMessage("Sync"),
"system": MessageLookupByLibrary.simpleMessage("System"), "system": MessageLookupByLibrary.simpleMessage("System"),
"systemApp": MessageLookupByLibrary.simpleMessage("System APP"),
"systemFont": MessageLookupByLibrary.simpleMessage("System font"), "systemFont": MessageLookupByLibrary.simpleMessage("System font"),
"systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"), "systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage( "systemProxyDesc": MessageLookupByLibrary.simpleMessage(
@@ -627,6 +651,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Enabling it will allow TCP concurrency", "Enabling it will allow TCP concurrency",
), ),
"testUrl": MessageLookupByLibrary.simpleMessage("Test url"), "testUrl": MessageLookupByLibrary.simpleMessage("Test url"),
"textScale": MessageLookupByLibrary.simpleMessage("Text Scaling"),
"theme": MessageLookupByLibrary.simpleMessage("Theme"), "theme": MessageLookupByLibrary.simpleMessage("Theme"),
"themeColor": MessageLookupByLibrary.simpleMessage("Theme color"), "themeColor": MessageLookupByLibrary.simpleMessage("Theme color"),
"themeDesc": MessageLookupByLibrary.simpleMessage( "themeDesc": MessageLookupByLibrary.simpleMessage(

View File

@@ -104,6 +104,7 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdate": MessageLookupByLibrary.simpleMessage("更新を確認"), "checkUpdate": MessageLookupByLibrary.simpleMessage("更新を確認"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("アプリは最新版です"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("アプリは最新版です"),
"checking": MessageLookupByLibrary.simpleMessage("確認中..."), "checking": MessageLookupByLibrary.simpleMessage("確認中..."),
"clearData": MessageLookupByLibrary.simpleMessage("データを消去"),
"clipboardExport": MessageLookupByLibrary.simpleMessage("クリップボードにエクスポート"), "clipboardExport": MessageLookupByLibrary.simpleMessage("クリップボードにエクスポート"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("クリップボードからインポート"), "clipboardImport": MessageLookupByLibrary.simpleMessage("クリップボードからインポート"),
"colorExists": MessageLookupByLibrary.simpleMessage("この色は既に存在します"), "colorExists": MessageLookupByLibrary.simpleMessage("この色は既に存在します"),
@@ -117,6 +118,7 @@ class MessageLookup extends MessageLookupByLibrary {
"connections": MessageLookupByLibrary.simpleMessage("接続"), "connections": MessageLookupByLibrary.simpleMessage("接続"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("現在の接続データを表示"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("現在の接続データを表示"),
"connectivity": MessageLookupByLibrary.simpleMessage("接続性:"), "connectivity": MessageLookupByLibrary.simpleMessage("接続性:"),
"contactMe": MessageLookupByLibrary.simpleMessage("連絡する"),
"content": MessageLookupByLibrary.simpleMessage("内容"), "content": MessageLookupByLibrary.simpleMessage("内容"),
"contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容は必須です"), "contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容は必須です"),
"contentScheme": MessageLookupByLibrary.simpleMessage("コンテンツテーマ"), "contentScheme": MessageLookupByLibrary.simpleMessage("コンテンツテーマ"),
@@ -127,6 +129,7 @@ class MessageLookup extends MessageLookupByLibrary {
"core": MessageLookupByLibrary.simpleMessage("コア"), "core": MessageLookupByLibrary.simpleMessage("コア"),
"coreInfo": MessageLookupByLibrary.simpleMessage("コア情報"), "coreInfo": MessageLookupByLibrary.simpleMessage("コア情報"),
"country": MessageLookupByLibrary.simpleMessage(""), "country": MessageLookupByLibrary.simpleMessage(""),
"crashTest": MessageLookupByLibrary.simpleMessage("クラッシュテスト"),
"create": MessageLookupByLibrary.simpleMessage("作成"), "create": MessageLookupByLibrary.simpleMessage("作成"),
"cut": MessageLookupByLibrary.simpleMessage("切り取り"), "cut": MessageLookupByLibrary.simpleMessage("切り取り"),
"dark": MessageLookupByLibrary.simpleMessage("ダーク"), "dark": MessageLookupByLibrary.simpleMessage("ダーク"),
@@ -150,6 +153,10 @@ class MessageLookup extends MessageLookupByLibrary {
"ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。", "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。",
), ),
"detectionTip": MessageLookupByLibrary.simpleMessage("サードパーティAPIに依存参考値"), "detectionTip": MessageLookupByLibrary.simpleMessage("サードパーティAPIに依存参考値"),
"developerMode": MessageLookupByLibrary.simpleMessage("デベロッパーモード"),
"developerModeEnableTip": MessageLookupByLibrary.simpleMessage(
"デベロッパーモードが有効になりました。",
),
"direct": MessageLookupByLibrary.simpleMessage("ダイレクト"), "direct": MessageLookupByLibrary.simpleMessage("ダイレクト"),
"disclaimer": MessageLookupByLibrary.simpleMessage("免責事項"), "disclaimer": MessageLookupByLibrary.simpleMessage("免責事項"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "disclaimerDesc": MessageLookupByLibrary.simpleMessage(
@@ -230,6 +237,7 @@ class MessageLookup extends MessageLookupByLibrary {
"init": MessageLookupByLibrary.simpleMessage("初期化"), "init": MessageLookupByLibrary.simpleMessage("初期化"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("正しいホットキーを入力"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("正しいホットキーを入力"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("インテリジェント選択"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("インテリジェント選択"),
"internet": MessageLookupByLibrary.simpleMessage("インターネット"),
"intranetIP": MessageLookupByLibrary.simpleMessage("イントラネットIP"), "intranetIP": MessageLookupByLibrary.simpleMessage("イントラネットIP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"), "ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("有効化するとIPv6トラフィックを受信可能"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("有効化するとIPv6トラフィックを受信可能"),
@@ -254,10 +262,13 @@ class MessageLookup extends MessageLookupByLibrary {
"logcatDesc": MessageLookupByLibrary.simpleMessage("無効化するとログエントリを非表示"), "logcatDesc": MessageLookupByLibrary.simpleMessage("無効化するとログエントリを非表示"),
"logs": MessageLookupByLibrary.simpleMessage("ログ"), "logs": MessageLookupByLibrary.simpleMessage("ログ"),
"logsDesc": MessageLookupByLibrary.simpleMessage("ログキャプチャ記録"), "logsDesc": MessageLookupByLibrary.simpleMessage("ログキャプチャ記録"),
"logsTest": MessageLookupByLibrary.simpleMessage("ログテスト"),
"loopback": MessageLookupByLibrary.simpleMessage("ループバック解除ツール"), "loopback": MessageLookupByLibrary.simpleMessage("ループバック解除ツール"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("UWPループバック解除用"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("UWPループバック解除用"),
"loose": MessageLookupByLibrary.simpleMessage(""), "loose": MessageLookupByLibrary.simpleMessage(""),
"memoryInfo": MessageLookupByLibrary.simpleMessage("メモリ情報"), "memoryInfo": MessageLookupByLibrary.simpleMessage("メモリ情報"),
"messageTest": MessageLookupByLibrary.simpleMessage("メッセージテスト"),
"messageTestTip": MessageLookupByLibrary.simpleMessage("これはメッセージです。"),
"min": MessageLookupByLibrary.simpleMessage("最小化"), "min": MessageLookupByLibrary.simpleMessage("最小化"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("終了時に最小化"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("終了時に最小化"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
@@ -287,6 +298,7 @@ class MessageLookup extends MessageLookupByLibrary {
"noInfo": MessageLookupByLibrary.simpleMessage("情報なし"), "noInfo": MessageLookupByLibrary.simpleMessage("情報なし"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("追加情報なし"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("追加情報なし"),
"noNetwork": MessageLookupByLibrary.simpleMessage("ネットワークなし"), "noNetwork": MessageLookupByLibrary.simpleMessage("ネットワークなし"),
"noNetworkApp": MessageLookupByLibrary.simpleMessage("ネットワークなしアプリ"),
"noProxy": MessageLookupByLibrary.simpleMessage("プロキシなし"), "noProxy": MessageLookupByLibrary.simpleMessage("プロキシなし"),
"noProxyDesc": MessageLookupByLibrary.simpleMessage( "noProxyDesc": MessageLookupByLibrary.simpleMessage(
"プロファイルを作成するか、有効なプロファイルを追加してください", "プロファイルを作成するか、有効なプロファイルを追加してください",
@@ -384,6 +396,11 @@ class MessageLookup extends MessageLookupByLibrary {
"recovery": MessageLookupByLibrary.simpleMessage("復元"), "recovery": MessageLookupByLibrary.simpleMessage("復元"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("全データ復元"), "recoveryAll": MessageLookupByLibrary.simpleMessage("全データ復元"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("プロファイルのみ復元"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("プロファイルのみ復元"),
"recoveryStrategy": MessageLookupByLibrary.simpleMessage("リカバリー戦略"),
"recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage("互換性"),
"recoveryStrategy_override": MessageLookupByLibrary.simpleMessage(
"オーバーライド",
),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("復元成功"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("復元成功"),
"redo": MessageLookupByLibrary.simpleMessage("やり直す"), "redo": MessageLookupByLibrary.simpleMessage("やり直す"),
"regExp": MessageLookupByLibrary.simpleMessage("正規表現"), "regExp": MessageLookupByLibrary.simpleMessage("正規表現"),
@@ -452,6 +469,7 @@ class MessageLookup extends MessageLookupByLibrary {
"submit": MessageLookupByLibrary.simpleMessage("送信"), "submit": MessageLookupByLibrary.simpleMessage("送信"),
"sync": MessageLookupByLibrary.simpleMessage("同期"), "sync": MessageLookupByLibrary.simpleMessage("同期"),
"system": MessageLookupByLibrary.simpleMessage("システム"), "system": MessageLookupByLibrary.simpleMessage("システム"),
"systemApp": MessageLookupByLibrary.simpleMessage("システムアプリ"),
"systemFont": MessageLookupByLibrary.simpleMessage("システムフォント"), "systemFont": MessageLookupByLibrary.simpleMessage("システムフォント"),
"systemProxy": MessageLookupByLibrary.simpleMessage("システムプロキシ"), "systemProxy": MessageLookupByLibrary.simpleMessage("システムプロキシ"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage( "systemProxyDesc": MessageLookupByLibrary.simpleMessage(
@@ -463,6 +481,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP並列処理"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP並列処理"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("TCP並列処理を許可"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("TCP並列処理を許可"),
"testUrl": MessageLookupByLibrary.simpleMessage("URLテスト"), "testUrl": MessageLookupByLibrary.simpleMessage("URLテスト"),
"textScale": MessageLookupByLibrary.simpleMessage("テキストスケーリング"),
"theme": MessageLookupByLibrary.simpleMessage("テーマ"), "theme": MessageLookupByLibrary.simpleMessage("テーマ"),
"themeColor": MessageLookupByLibrary.simpleMessage("テーマカラー"), "themeColor": MessageLookupByLibrary.simpleMessage("テーマカラー"),
"themeDesc": MessageLookupByLibrary.simpleMessage("ダークモードの設定、色の調整"), "themeDesc": MessageLookupByLibrary.simpleMessage("ダークモードの設定、色の調整"),

View File

@@ -148,6 +148,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Текущее приложение уже является последней версией", "Текущее приложение уже является последней версией",
), ),
"checking": MessageLookupByLibrary.simpleMessage("Проверка..."), "checking": MessageLookupByLibrary.simpleMessage("Проверка..."),
"clearData": MessageLookupByLibrary.simpleMessage("Очистить данные"),
"clipboardExport": MessageLookupByLibrary.simpleMessage( "clipboardExport": MessageLookupByLibrary.simpleMessage(
"Экспорт в буфер обмена", "Экспорт в буфер обмена",
), ),
@@ -169,6 +170,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Просмотр текущих данных о соединениях", "Просмотр текущих данных о соединениях",
), ),
"connectivity": MessageLookupByLibrary.simpleMessage("Связь:"), "connectivity": MessageLookupByLibrary.simpleMessage("Связь:"),
"contactMe": MessageLookupByLibrary.simpleMessage("Свяжитесь со мной"),
"content": MessageLookupByLibrary.simpleMessage("Содержание"), "content": MessageLookupByLibrary.simpleMessage("Содержание"),
"contentEmptyTip": MessageLookupByLibrary.simpleMessage( "contentEmptyTip": MessageLookupByLibrary.simpleMessage(
"Содержание не может быть пустым", "Содержание не может быть пустым",
@@ -183,6 +185,7 @@ class MessageLookup extends MessageLookupByLibrary {
"core": MessageLookupByLibrary.simpleMessage("Ядро"), "core": MessageLookupByLibrary.simpleMessage("Ядро"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Информация о ядре"), "coreInfo": MessageLookupByLibrary.simpleMessage("Информация о ядре"),
"country": MessageLookupByLibrary.simpleMessage("Страна"), "country": MessageLookupByLibrary.simpleMessage("Страна"),
"crashTest": MessageLookupByLibrary.simpleMessage("Тест на сбои"),
"create": MessageLookupByLibrary.simpleMessage("Создать"), "create": MessageLookupByLibrary.simpleMessage("Создать"),
"cut": MessageLookupByLibrary.simpleMessage("Вырезать"), "cut": MessageLookupByLibrary.simpleMessage("Вырезать"),
"dark": MessageLookupByLibrary.simpleMessage("Темный"), "dark": MessageLookupByLibrary.simpleMessage("Темный"),
@@ -216,6 +219,10 @@ class MessageLookup extends MessageLookupByLibrary {
"detectionTip": MessageLookupByLibrary.simpleMessage( "detectionTip": MessageLookupByLibrary.simpleMessage(
"Опирается на сторонний API, только для справки", "Опирается на сторонний API, только для справки",
), ),
"developerMode": MessageLookupByLibrary.simpleMessage("Режим разработчика"),
"developerModeEnableTip": MessageLookupByLibrary.simpleMessage(
"Режим разработчика активирован.",
),
"direct": MessageLookupByLibrary.simpleMessage("Прямой"), "direct": MessageLookupByLibrary.simpleMessage("Прямой"),
"disclaimer": MessageLookupByLibrary.simpleMessage( "disclaimer": MessageLookupByLibrary.simpleMessage(
"Отказ от ответственности", "Отказ от ответственности",
@@ -342,6 +349,7 @@ class MessageLookup extends MessageLookupByLibrary {
"intelligentSelected": MessageLookupByLibrary.simpleMessage( "intelligentSelected": MessageLookupByLibrary.simpleMessage(
"Интеллектуальный выбор", "Интеллектуальный выбор",
), ),
"internet": MessageLookupByLibrary.simpleMessage("Интернет"),
"intranetIP": MessageLookupByLibrary.simpleMessage("Внутренний IP"), "intranetIP": MessageLookupByLibrary.simpleMessage("Внутренний IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"), "ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage( "ipv6Desc": MessageLookupByLibrary.simpleMessage(
@@ -378,6 +386,7 @@ class MessageLookup extends MessageLookupByLibrary {
), ),
"logs": MessageLookupByLibrary.simpleMessage("Логи"), "logs": MessageLookupByLibrary.simpleMessage("Логи"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Записи захвата логов"), "logsDesc": MessageLookupByLibrary.simpleMessage("Записи захвата логов"),
"logsTest": MessageLookupByLibrary.simpleMessage("Тест журналов"),
"loopback": MessageLookupByLibrary.simpleMessage( "loopback": MessageLookupByLibrary.simpleMessage(
"Инструмент разблокировки Loopback", "Инструмент разблокировки Loopback",
), ),
@@ -386,6 +395,10 @@ class MessageLookup extends MessageLookupByLibrary {
), ),
"loose": MessageLookupByLibrary.simpleMessage("Свободный"), "loose": MessageLookupByLibrary.simpleMessage("Свободный"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("Информация о памяти"), "memoryInfo": MessageLookupByLibrary.simpleMessage("Информация о памяти"),
"messageTest": MessageLookupByLibrary.simpleMessage(
"Тестирование сообщения",
),
"messageTestTip": MessageLookupByLibrary.simpleMessage("Это сообщение."),
"min": MessageLookupByLibrary.simpleMessage("Мин"), "min": MessageLookupByLibrary.simpleMessage("Мин"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage( "minimizeOnExit": MessageLookupByLibrary.simpleMessage(
"Свернуть при выходе", "Свернуть при выходе",
@@ -427,6 +440,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Нет дополнительной информации", "Нет дополнительной информации",
), ),
"noNetwork": MessageLookupByLibrary.simpleMessage("Нет сети"), "noNetwork": MessageLookupByLibrary.simpleMessage("Нет сети"),
"noNetworkApp": MessageLookupByLibrary.simpleMessage("Приложение без сети"),
"noProxy": MessageLookupByLibrary.simpleMessage("Нет прокси"), "noProxy": MessageLookupByLibrary.simpleMessage("Нет прокси"),
"noProxyDesc": MessageLookupByLibrary.simpleMessage( "noProxyDesc": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, создайте профиль или добавьте действительный профиль", "Пожалуйста, создайте профиль или добавьте действительный профиль",
@@ -560,6 +574,15 @@ class MessageLookup extends MessageLookupByLibrary {
"recoveryProfiles": MessageLookupByLibrary.simpleMessage( "recoveryProfiles": MessageLookupByLibrary.simpleMessage(
"Только восстановление профилей", "Только восстановление профилей",
), ),
"recoveryStrategy": MessageLookupByLibrary.simpleMessage(
"Стратегия восстановления",
),
"recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage(
"Совместимый",
),
"recoveryStrategy_override": MessageLookupByLibrary.simpleMessage(
"Переопределение",
),
"recoverySuccess": MessageLookupByLibrary.simpleMessage( "recoverySuccess": MessageLookupByLibrary.simpleMessage(
"Восстановление успешно", "Восстановление успешно",
), ),
@@ -650,6 +673,7 @@ class MessageLookup extends MessageLookupByLibrary {
"submit": MessageLookupByLibrary.simpleMessage("Отправить"), "submit": MessageLookupByLibrary.simpleMessage("Отправить"),
"sync": MessageLookupByLibrary.simpleMessage("Синхронизация"), "sync": MessageLookupByLibrary.simpleMessage("Синхронизация"),
"system": MessageLookupByLibrary.simpleMessage("Система"), "system": MessageLookupByLibrary.simpleMessage("Система"),
"systemApp": MessageLookupByLibrary.simpleMessage("Системное приложение"),
"systemFont": MessageLookupByLibrary.simpleMessage("Системный шрифт"), "systemFont": MessageLookupByLibrary.simpleMessage("Системный шрифт"),
"systemProxy": MessageLookupByLibrary.simpleMessage("Системный прокси"), "systemProxy": MessageLookupByLibrary.simpleMessage("Системный прокси"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage( "systemProxyDesc": MessageLookupByLibrary.simpleMessage(
@@ -665,6 +689,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Включение позволит использовать параллелизм TCP", "Включение позволит использовать параллелизм TCP",
), ),
"testUrl": MessageLookupByLibrary.simpleMessage("Тест URL"), "testUrl": MessageLookupByLibrary.simpleMessage("Тест URL"),
"textScale": MessageLookupByLibrary.simpleMessage("Масштабирование текста"),
"theme": MessageLookupByLibrary.simpleMessage("Тема"), "theme": MessageLookupByLibrary.simpleMessage("Тема"),
"themeColor": MessageLookupByLibrary.simpleMessage("Цвет темы"), "themeColor": MessageLookupByLibrary.simpleMessage("Цвет темы"),
"themeDesc": MessageLookupByLibrary.simpleMessage( "themeDesc": MessageLookupByLibrary.simpleMessage(

View File

@@ -94,6 +94,7 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."), "checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"clearData": MessageLookupByLibrary.simpleMessage("清除数据"),
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"), "clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"), "clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
"colorExists": MessageLookupByLibrary.simpleMessage("该颜色已存在"), "colorExists": MessageLookupByLibrary.simpleMessage("该颜色已存在"),
@@ -107,6 +108,7 @@ class MessageLookup extends MessageLookupByLibrary {
"connections": MessageLookupByLibrary.simpleMessage("连接"), "connections": MessageLookupByLibrary.simpleMessage("连接"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"contactMe": MessageLookupByLibrary.simpleMessage("联系我"),
"content": MessageLookupByLibrary.simpleMessage("内容"), "content": MessageLookupByLibrary.simpleMessage("内容"),
"contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容不能为空"), "contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容不能为空"),
"contentScheme": MessageLookupByLibrary.simpleMessage("内容主题"), "contentScheme": MessageLookupByLibrary.simpleMessage("内容主题"),
@@ -117,6 +119,7 @@ class MessageLookup extends MessageLookupByLibrary {
"core": MessageLookupByLibrary.simpleMessage("内核"), "core": MessageLookupByLibrary.simpleMessage("内核"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"country": MessageLookupByLibrary.simpleMessage("区域"), "country": MessageLookupByLibrary.simpleMessage("区域"),
"crashTest": MessageLookupByLibrary.simpleMessage("崩溃测试"),
"create": MessageLookupByLibrary.simpleMessage("创建"), "create": MessageLookupByLibrary.simpleMessage("创建"),
"cut": MessageLookupByLibrary.simpleMessage("剪切"), "cut": MessageLookupByLibrary.simpleMessage("剪切"),
"dark": MessageLookupByLibrary.simpleMessage("深色"), "dark": MessageLookupByLibrary.simpleMessage("深色"),
@@ -136,6 +139,8 @@ class MessageLookup extends MessageLookupByLibrary {
"基于ClashMeta的多平台代理客户端简单易用开源无广告。", "基于ClashMeta的多平台代理客户端简单易用开源无广告。",
), ),
"detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方api仅供参考"), "detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方api仅供参考"),
"developerMode": MessageLookupByLibrary.simpleMessage("开发者模式"),
"developerModeEnableTip": MessageLookupByLibrary.simpleMessage("开发者模式已启用。"),
"direct": MessageLookupByLibrary.simpleMessage("直连"), "direct": MessageLookupByLibrary.simpleMessage("直连"),
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"), "disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "disclaimerDesc": MessageLookupByLibrary.simpleMessage(
@@ -206,6 +211,7 @@ class MessageLookup extends MessageLookupByLibrary {
"init": MessageLookupByLibrary.simpleMessage("初始化"), "init": MessageLookupByLibrary.simpleMessage("初始化"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
"internet": MessageLookupByLibrary.simpleMessage("互联网"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"), "ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
@@ -228,10 +234,13 @@ class MessageLookup extends MessageLookupByLibrary {
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"), "logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"logsTest": MessageLookupByLibrary.simpleMessage("日志测试"),
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"), "loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
"loose": MessageLookupByLibrary.simpleMessage("宽松"), "loose": MessageLookupByLibrary.simpleMessage("宽松"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"), "memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"),
"messageTest": MessageLookupByLibrary.simpleMessage("消息测试"),
"messageTestTip": MessageLookupByLibrary.simpleMessage("这是一条消息。"),
"min": MessageLookupByLibrary.simpleMessage("最小"), "min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
@@ -257,6 +266,7 @@ class MessageLookup extends MessageLookupByLibrary {
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"), "noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"), "noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
"noNetworkApp": MessageLookupByLibrary.simpleMessage("无网络应用"),
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"), "noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
"noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"), "noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
"noResolve": MessageLookupByLibrary.simpleMessage("不解析IP"), "noResolve": MessageLookupByLibrary.simpleMessage("不解析IP"),
@@ -338,6 +348,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoveryStrategy": MessageLookupByLibrary.simpleMessage("恢复策略"),
"recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage("兼容"),
"recoveryStrategy_override": MessageLookupByLibrary.simpleMessage("覆盖"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"redo": MessageLookupByLibrary.simpleMessage("重做"), "redo": MessageLookupByLibrary.simpleMessage("重做"),
"regExp": MessageLookupByLibrary.simpleMessage("正则"), "regExp": MessageLookupByLibrary.simpleMessage("正则"),
@@ -398,6 +411,7 @@ class MessageLookup extends MessageLookupByLibrary {
"submit": MessageLookupByLibrary.simpleMessage("提交"), "submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"), "sync": MessageLookupByLibrary.simpleMessage("同步"),
"system": MessageLookupByLibrary.simpleMessage("系统"), "system": MessageLookupByLibrary.simpleMessage("系统"),
"systemApp": MessageLookupByLibrary.simpleMessage("系统应用"),
"systemFont": MessageLookupByLibrary.simpleMessage("系统字体"), "systemFont": MessageLookupByLibrary.simpleMessage("系统字体"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"),
@@ -407,6 +421,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"), "testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"textScale": MessageLookupByLibrary.simpleMessage("文本缩放"),
"theme": MessageLookupByLibrary.simpleMessage("主题"), "theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),

View File

@@ -3004,6 +3004,121 @@ class AppLocalizations {
args: [], args: [],
); );
} }
/// `Developer mode`
String get developerMode {
return Intl.message(
'Developer mode',
name: 'developerMode',
desc: '',
args: [],
);
}
/// `Developer mode is enabled.`
String get developerModeEnableTip {
return Intl.message(
'Developer mode is enabled.',
name: 'developerModeEnableTip',
desc: '',
args: [],
);
}
/// `Message test`
String get messageTest {
return Intl.message(
'Message test',
name: 'messageTest',
desc: '',
args: [],
);
}
/// `This is a message.`
String get messageTestTip {
return Intl.message(
'This is a message.',
name: 'messageTestTip',
desc: '',
args: [],
);
}
/// `Crash test`
String get crashTest {
return Intl.message('Crash test', name: 'crashTest', desc: '', args: []);
}
/// `Clear Data`
String get clearData {
return Intl.message('Clear Data', name: 'clearData', desc: '', args: []);
}
/// `Text Scaling`
String get textScale {
return Intl.message('Text Scaling', name: 'textScale', desc: '', args: []);
}
/// `Internet`
String get internet {
return Intl.message('Internet', name: 'internet', desc: '', args: []);
}
/// `System APP`
String get systemApp {
return Intl.message('System APP', name: 'systemApp', desc: '', args: []);
}
/// `No network APP`
String get noNetworkApp {
return Intl.message(
'No network APP',
name: 'noNetworkApp',
desc: '',
args: [],
);
}
/// `Contact me`
String get contactMe {
return Intl.message('Contact me', name: 'contactMe', desc: '', args: []);
}
/// `Recovery strategy`
String get recoveryStrategy {
return Intl.message(
'Recovery strategy',
name: 'recoveryStrategy',
desc: '',
args: [],
);
}
/// `Override`
String get recoveryStrategy_override {
return Intl.message(
'Override',
name: 'recoveryStrategy_override',
desc: '',
args: [],
);
}
/// `Compatible`
String get recoveryStrategy_compatible {
return Intl.message(
'Compatible',
name: 'recoveryStrategy_compatible',
desc: '',
args: [],
);
}
/// `Logs test`
String get logsTest {
return Intl.message('Logs test', name: 'logsTest', desc: '', args: []);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -26,7 +26,6 @@ Future<void> main() async {
await globalState.initApp(version); await globalState.initApp(version);
await android?.init(); await android?.init();
await window?.init(version); await window?.init(version);
globalState.isPre = const String.fromEnvironment("APP_ENV") != 'stable';
HttpOverrides.global = FlClashHttpOverrides(); HttpOverrides.global = FlClashHttpOverrides();
runApp(ProviderScope( runApp(ProviderScope(
child: const Application(), child: const Application(),

View File

@@ -1,11 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AppStateManager extends StatefulWidget { class AppStateManager extends ConsumerStatefulWidget {
final Widget child; final Widget child;
const AppStateManager({ const AppStateManager({
@@ -14,15 +16,31 @@ class AppStateManager extends StatefulWidget {
}); });
@override @override
State<AppStateManager> createState() => _AppStateManagerState(); ConsumerState<AppStateManager> createState() => _AppStateManagerState();
} }
class _AppStateManagerState extends State<AppStateManager> class _AppStateManagerState extends ConsumerState<AppStateManager>
with WidgetsBindingObserver { with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
ref.listenManual(layoutChangeProvider, (prev, next) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (prev != next) {
globalState.cacheHeightMap = {};
}
});
});
ref.listenManual(
checkIpProvider,
(prev, next) {
if (prev != next && next.b) {
detectionState.startCheck();
}
},
fireImmediately: true,
);
} }
@override @override

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