Compare commits

...

8 Commits

Author SHA1 Message Date
chen08209
f6d9ed11d9 Fix windows tray issues
Optimize windows logic
2024-08-25 23:40:13 +08:00
chen08209
c38a671d57 Optimize app logic
Support windows administrator auto launch

Support android close vpn
2024-08-22 19:56:19 +08:00
chen08209
75af47aead Change flutter version 2024-08-15 14:34:02 +08:00
chen08209
8dafe3b0ec Support profiles sort
Support windows country flags display

Optimize proxies page and profiles page columns
2024-08-15 14:18:33 +08:00
chen08209
813198a21d Update flutter version 2024-08-11 17:45:57 +08:00
chen08209
68dd262fef Update version 2024-08-11 17:09:31 +08:00
chen08209
5ef020db73 Update timeout time 2024-08-11 17:08:51 +08:00
chen08209
e3c9035903 Update access control page
Fix bug
2024-08-11 17:08:46 +08:00
96 changed files with 4864 additions and 2213 deletions

View File

@@ -87,7 +87,7 @@ jobs:
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.x' flutter-version: 3.22.x
channel: 'stable' channel: 'stable'
cache: true cache: true
@@ -136,18 +136,25 @@ jobs:
gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog" gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog"
echo -e "\n\n</details>" >> release.md echo -e "\n\n</details>" >> release.md
fi fi
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ./dist/* files: ./dist/*
body_path: './release.md' body_path: './release.md'
- name: Create Fdroid Source Dir
run: |
mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully"
- name: Push to fdroid repo - name: Push to fdroid repo
uses: cpina/github-action-push-to-another-repository@v1.7.2 uses: cpina/github-action-push-to-another-repository@v1.7.2
env: env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with: with:
source-directory: ./dist/ source-directory: ./tmp/
destination-github-username: chen08209 destination-github-username: chen08209
destination-repository-name: FlClash-fdroid-repo destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]' user-name: 'github-actions[bot]'

View File

@@ -102,6 +102,9 @@ flutter {
dependencies { dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'com.google.code.gson:gson:2.10' implementation 'com.google.code.gson:gson:2.10'
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
exclude group: "com.google.guava", module: "guava"
}
} }

View File

@@ -2,11 +2,11 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -14,18 +14,20 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" <uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" /> tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash" android:label="FlClash"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name="com.follow.clash.MainActivity" android:name="com.follow.clash.MainActivity"
@@ -56,17 +58,17 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="clash"/> <data android:scheme="clash" />
<data android:scheme="clashmeta"/> <data android:scheme="clashmeta" />
<data android:scheme="flclash"/> <data android:scheme="flclash" />
<data android:host="install-config"/> <data android:host="install-config" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- <meta-data--> <!-- <meta-data-->
<!-- android:name="io.flutter.embedding.android.EnableImpeller"--> <!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
<!-- android:value="true" />--> <!-- android:value="true" />-->
<activity <activity
android:name=".TempActivity" android:name=".TempActivity"
@@ -75,11 +77,10 @@
<service <service
android:name=".services.FlClashTileService" android:name=".services.FlClashTileService"
android:exported="true" android:exported="true"
android:icon="@drawable/ic_stat_name"
android:foregroundServiceType="specialUse" android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:label="FlClash" android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
>
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
@@ -114,13 +115,17 @@
android:name=".services.FlClashVpnService" android:name=".services.FlClashVpnService"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse" android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE">
>
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService" /> <action android:name="android.net.VpnService" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".services.FlClashService"
android:exported="false"
android:foregroundServiceType="specialUse" />
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />

View File

@@ -0,0 +1,9 @@
package com.follow.clash
import com.follow.clash.models.Props
interface BaseServiceInterface {
fun start(port: Int, props: Props?): Int?
fun stop()
fun startForeground(title: String, content: String)
}

View File

@@ -1,10 +1,10 @@
package com.follow.clash package com.follow.clash
import android.content.Context import android.content.Context
import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@@ -22,6 +22,7 @@ enum class RunState {
object GlobalState { object GlobalState {
private val lock = ReentrantLock() private val lock = ReentrantLock()
val runLock = ReentrantLock()
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP) val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null var flutterEngine: FlutterEngine? = null
@@ -37,6 +38,11 @@ object GlobalState {
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin? return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
} }
fun getCurrentVPNPlugin(): VpnPlugin? {
val currentEngine = if (serviceEngine != null) serviceEngine else flutterEngine
return currentEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
}
fun destroyServiceEngine() { fun destroyServiceEngine() {
serviceEngine?.destroy() serviceEngine?.destroy()
serviceEngine = null serviceEngine = null
@@ -47,9 +53,10 @@ object GlobalState {
lock.withLock { lock.withLock {
destroyServiceEngine() destroyServiceEngine()
serviceEngine = FlutterEngine(context) serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(ProxyPlugin()) serviceEngine?.plugins?.add(VpnPlugin())
serviceEngine?.plugins?.add(AppPlugin()) serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin()) serviceEngine?.plugins?.add(TilePlugin())
serviceEngine?.plugins?.add(ServicePlugin())
val vpnService = DartExecutor.DartEntrypoint( val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(), FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService" "vpnService"

View File

@@ -2,7 +2,8 @@ package com.follow.clash
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@@ -12,7 +13,8 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin()) flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(ProxyPlugin()) flutterEngine.plugins.add(VpnPlugin())
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin()) flutterEngine.plugins.add(TilePlugin())
GlobalState.flutterEngine = flutterEngine GlobalState.flutterEngine = flutterEngine
} }

View File

@@ -1,18 +1,28 @@
package com.follow.clash.extensions package com.follow.clash.extensions
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build
import android.system.OsConstants.IPPROTO_TCP import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64 import android.util.Base64
import java.net.URL import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.models.Metadata import com.follow.clash.models.Metadata
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.net.InetAddress
import java.net.InetSocketAddress
suspend fun Drawable.getBase64(): String { suspend fun Drawable.getBase64(): String {
@@ -31,7 +41,6 @@ fun Metadata.getProtocol(): Int? {
return null return null
} }
fun String.getInetSocketAddress(): InetSocketAddress { private val CHANNEL = "FlClash"
val url = URL("https://$this")
return InetSocketAddress(InetAddress.getByName(url.host), url.port) private val notificationId: Int = 1
}

View File

@@ -1,7 +1,10 @@
package com.follow.clash.models package com.follow.clash.models
import java.util.Date
data class Package( data class Package(
val packageName: String, val packageName: String,
val label: String, val label: String,
val isSystem:Boolean val isSystem: Boolean,
val firstInstallTime: Long,
) )

View File

@@ -12,6 +12,7 @@ data class AccessControl(
) )
data class Props( data class Props(
val enable: Boolean?,
val accessControl: AccessControl?, val accessControl: AccessControl?,
val allowBypass: Boolean?, val allowBypass: Boolean?,
val systemProxy: Boolean?, val systemProxy: Boolean?,

View File

@@ -6,11 +6,16 @@ import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.VpnService
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
@@ -19,6 +24,7 @@ import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Package import com.follow.clash.models.Package
import com.follow.clash.models.Process import com.follow.clash.models.Process
import com.google.gson.Gson import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -32,7 +38,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
@@ -40,7 +46,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var toast: Toast? = null private var toast: Toast? = null
private var context: Context? = null private lateinit var context: Context
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
@@ -48,14 +54,78 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var connectivity: ConnectivityManager? = null private var connectivity: ConnectivityManager? = null
private var vpnCallBack: (() -> Unit)? = null
private val iconMap = mutableMapOf<String, String?>() private val iconMap = mutableMapOf<String, String?>()
private val packages = mutableListOf<Package>()
private val skipPrefixList = listOf(
"com.google",
"com.android.chrome",
"com.android.vending",
"com.microsoft",
"com.apple",
"com.zhiliaoapp.musically", // Banned by China
)
private val chinaAppPrefixList = listOf(
"com.tencent",
"com.alibaba",
"com.umeng",
"com.qihoo",
"com.ali",
"com.alipay",
"com.amap",
"com.sina",
"com.weibo",
"com.vivo",
"com.xiaomi",
"com.huawei",
"com.taobao",
"com.secneo",
"s.h.e.l.l",
"com.stub",
"com.kiwisec",
"com.secshell",
"com.wrapper",
"cn.securitystack",
"com.mogosec",
"com.secoen",
"com.netease",
"com.mx",
"com.qq.e",
"com.baidu",
"com.bytedance",
"com.bugly",
"com.miui",
"com.oppo",
"com.coloros",
"com.iqoo",
"com.meizu",
"com.gionee",
"cn.nubia",
"com.oplus",
"andes.oplus",
"com.unionpay",
"cn.wps"
)
private val chinaAppRegex by lazy {
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
}
val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
private var isBlockNotification: Boolean = false
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext; context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -88,7 +158,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
"getPackages" -> { "getPackages" -> {
scope.launch { scope.launch {
result.success(getPackages()) result.success(getPackagesToJson())
}
}
"getChinaPackageNames" -> {
scope.launch {
result.success(getChinaPackageNames())
} }
} }
@@ -107,7 +183,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
if (iconMap["default"] == null) { if (iconMap["default"] == null) {
iconMap["default"] = iconMap["default"] =
context?.packageManager?.defaultActivityIcon?.getBase64() context.packageManager?.defaultActivityIcon?.getBase64()
} }
result.success(iconMap["default"]) result.success(iconMap["default"])
return@launch return@launch
@@ -134,12 +210,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(null) result.success(null)
return@withContext return@withContext
} }
if (context == null) {
result.success(null)
return@withContext
}
if (connectivity == null) { if (connectivity == null) {
connectivity = context!!.getSystemService<ConnectivityManager>() connectivity = context.getSystemService<ConnectivityManager>()
} }
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort) val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress( val dst = InetSocketAddress(
@@ -155,7 +227,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(null) result.success(null)
return@withContext return@withContext
} }
val packages = context?.packageManager?.getPackagesForUid(uid) val packages = context.packageManager?.getPackagesForUid(uid)
result.success(packages?.first()) result.success(packages?.first())
} }
} }
@@ -180,46 +252,43 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private fun openFile(path: String) { private fun openFile(path: String) {
context?.let { val file = File(path)
val file = File(path) val uri = FileProvider.getUriForFile(
val uri = FileProvider.getUriForFile( context,
it, "${context.packageName}.fileProvider",
"${it.packageName}.fileProvider", file
file )
)
val intent = Intent(Intent.ACTION_VIEW).setDataAndType( val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
uri,
"text/plain"
)
val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
val resInfoList = context.packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY
)
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
context.grantUriPermission(
packageName,
uri, uri,
"text/plain" flags
) )
}
val flags = try {
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION activity?.startActivity(intent)
} catch (e: Exception) {
val resInfoList = it.packageManager.queryIntentActivities( println(e)
intent, PackageManager.MATCH_DEFAULT_ONLY
)
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
it.grantUriPermission(
packageName,
uri,
flags
)
}
try {
activity?.startActivity(intent)
} catch (e: Exception) {
println(e)
}
} }
} }
private fun updateExcludeFromRecents(value: Boolean?) { private fun updateExcludeFromRecents(value: Boolean?) {
if (context == null) return val am = getSystemService(context, ActivityManager::class.java)
val am = getSystemService(context!!, ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull { val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId it.taskInfo.taskId == activity?.taskId
@@ -236,7 +305,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private suspend fun getPackageIcon(packageName: String): String? { private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context?.packageManager val packageManager = context.packageManager
if (iconMap[packageName] == null) { if (iconMap[packageName] == null) {
iconMap[packageName] = try { iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64() packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -248,32 +317,144 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
return iconMap[packageName] return iconMap[packageName]
} }
private suspend fun getPackages(): String { private fun getPackages(): List<Package> {
return withContext(Dispatchers.Default) { val packageManager = context.packageManager
val packageManager = context?.packageManager if (packages.isNotEmpty()) return packages;
val packages: List<Package>? = packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { it.packageName != context.packageName
it.packageName != context?.packageName || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true || it.packageName == "android"
|| it.packageName == "android"
}?.map { }?.map {
Package( Package(
packageName = it.packageName, packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(), label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1 isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
) firstInstallTime = it.firstInstallTime
} )
}?.let { packages.addAll(it) }
return packages;
}
private suspend fun getPackagesToJson(): String {
return withContext(Dispatchers.Default) {
Gson().toJson(getPackages())
}
}
private suspend fun getChinaPackageNames(): String {
return withContext(Dispatchers.Default) {
val packages: List<String> =
getPackages().map { it.packageName }.filter { isChinaPackage(it) }
Gson().toJson(packages) Gson().toJson(packages)
} }
} }
fun requestVpnPermission(context: Context, callBack: () -> Unit) {
vpnCallBack = callBack
val intent = VpnService.prepare(context)
if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
return;
}
vpnCallBack?.invoke()
}
fun requestNotificationsPermission(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
)
if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return
if (activity == null) return
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
return
}
}
}
private fun isChinaPackage(packageName: String): Boolean {
val packageManager = context.packageManager ?: return false
skipPrefixList.forEach {
if (packageName == it || packageName.startsWith("$it.")) return false
}
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
} else {
@Suppress("DEPRECATION")
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
if (packageName.matches(chinaAppRegex)) {
return true
}
try {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
)
} else {
@Suppress("DEPRECATION") packageManager.getPackageInfo(
packageName, packageManagerFlags
)
}
mutableListOf<ComponentInfo>().apply {
packageInfo.services?.let { addAll(it) }
packageInfo.activities?.let { addAll(it) }
packageInfo.receivers?.let { addAll(it) }
packageInfo.providers?.let { addAll(it) }
}.forEach {
if (it.name.matches(chinaAppRegex)) return true
}
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
for (packageEntry in it.entries()) {
if (packageEntry.name.startsWith("firebase-")) return false
}
for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
) {
continue
}
if (packageEntry.size > 15000000) {
return true
}
val input = it.getInputStream(packageEntry).buffered()
val dexFile = try {
DexBackedDexFile.fromInputStream(null, input)
} catch (e: Exception) {
return false
}
for (clazz in dexFile.classes) {
val clazzName =
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
.replace("$", ".")
if (clazzName.matches(chinaAppRegex)) return true
}
}
}
} catch (_: Exception) {
return false
}
return false
}
fun requestGc() { fun requestGc() {
channel.invokeMethod("gc", null) channel.invokeMethod("gc", null)
} }
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity; activity = binding.activity;
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
@@ -288,4 +469,25 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
channel.invokeMethod("exit", null) channel.invokeMethod("exit", null)
activity = null activity = null
} }
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) {
GlobalState.initServiceEngine(context)
vpnCallBack?.invoke()
}
}
return true
}
private fun onRequestPermissionsResultListener(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
isBlockNotification = true
}
return true
}
} }

View File

@@ -1,220 +0,0 @@
package com.follow.clash.plugins
import android.Manifest
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.Props
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private lateinit var flutterMethodChannel: MethodChannel
val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
private var activity: Activity? = null
private var context: Context? = null
private var flClashVpnService: FlClashVpnService? = null
private var port: Int = 7890
private var props: Props? = null
private var isBlockNotification: Boolean = false
private var isStart: Boolean = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as FlClashVpnService.LocalBinder
flClashVpnService = binder.getService()
if (isStart) {
startVpn()
} else {
flClashVpnService?.initServiceEngine()
}
}
override fun onServiceDisconnected(arg: ComponentName) {
flClashVpnService = null
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "proxy")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"initService" -> {
isStart = false
initService()
requestNotificationsPermission()
result.success(true)
}
"startProxy" -> {
isStart = true
port = call.argument<Int>("port")!!
val args = call.argument<String>("args")
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
startVpn()
result.success(true)
}
"stopProxy" -> {
stopVpn()
result.success(true)
}
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
flClashVpnService?.protect(fd)
result.success(true)
} else {
result.success(false)
}
}
"startForeground" -> {
val title = call.argument<String>("title") as String
val content = call.argument<String>("content") as String
startForeground(title, content)
result.success(true)
}
else -> {
result.notImplemented()
}
}
private fun initService() {
val intent = VpnService.prepare(context)
if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
} else {
if (flClashVpnService != null) {
flClashVpnService!!.initServiceEngine()
} else {
bindService()
}
}
}
private fun startVpn() {
if (flClashVpnService == null) {
bindService()
return
}
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
val intent = VpnService.prepare(context)
if (intent != null) {
stopVpn()
return
}
val fd = flClashVpnService?.start(port, props)
flutterMethodChannel.invokeMethod("started", fd)
}
private fun stopVpn() {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashVpnService?.stop()
GlobalState.destroyServiceEngine()
}
private fun startForeground(title: String, content: String) {
if (GlobalState.runState.value != RunState.START) return
flClashVpnService?.startForeground(title, content)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) {
bindService()
} else {
stopVpn()
}
}
return true
}
private fun onRequestPermissionsResultListener(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
isBlockNotification = true
}
return false
}
private fun requestNotificationsPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = context?.let {
ContextCompat.checkSelfPermission(
it,
Manifest.permission.POST_NOTIFICATIONS
)
}
if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return
if (activity == null) return
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
}
private fun bindService() {
val intent = Intent(context, FlClashVpnService::class.java)
context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}

View File

@@ -0,0 +1,47 @@
package com.follow.clash.plugins
import android.content.Context
import com.follow.clash.GlobalState
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"init" -> {
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
GlobalState.initServiceEngine(context)
result.success(true)
}
"destroy" -> {
handleDestroy()
result.success(true)
}
else -> {
result.notImplemented()
}
}
private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.stop()
GlobalState.destroyServiceEngine()
}
}

View File

@@ -0,0 +1,143 @@
package com.follow.clash.plugins
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.Props
import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlin.concurrent.withLock
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
private var flClashService: BaseServiceInterface? = null
private var port: Int = 7890
private var props: Props? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
flClashService = when (service) {
is FlClashVpnService.LocalBinder -> service.getService()
is FlClashService.LocalBinder -> service.getService()
else -> throw Exception("invalid binder")
}
start()
}
override fun onServiceDisconnected(arg: ComponentName) {
flClashService = null
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"start" -> {
port = call.argument<Int>("port")!!
val args = call.argument<String>("args")
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
when (props?.enable == true) {
true -> handleStartVpn()
false -> start()
}
result.success(true)
}
"stop" -> {
stop()
result.success(true)
}
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
if (flClashService is FlClashVpnService) {
(flClashService as FlClashVpnService).protect(fd)
}
result.success(true)
} else {
result.success(false)
}
}
"startForeground" -> {
val title = call.argument<String>("title") as String
val content = call.argument<String>("content") as String
startForeground(title, content)
result.success(true)
}
else -> {
result.notImplemented()
}
}
@SuppressLint("ForegroundServiceType")
fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
start()
}
}
@SuppressLint("ForegroundServiceType")
private fun startForeground(title: String, content: String) {
GlobalState.runLock.withLock {
if (GlobalState.runState.value != RunState.START) return
flClashService?.startForeground(title, content)
}
}
private fun start() {
if (flClashService == null) {
bindService()
return
}
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
val fd = flClashService?.start(port, props)
flutterMethodChannel.invokeMethod("started", fd)
}
}
fun stop() {
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashService?.stop()
}
GlobalState.destroyServiceEngine()
}
private fun bindService() {
val intent = when (props?.enable == true) {
true -> Intent(context, FlClashVpnService::class.java)
false -> Intent(context, FlClashService::class.java)
}
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}

View File

@@ -0,0 +1,104 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.MainActivity
import com.follow.clash.models.Props
@SuppressLint("WrongConstant")
class FlClashService : Service(), BaseServiceInterface {
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): FlClashService = this@FlClashService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
private val CHANNEL = "FlClash"
private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, 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
}
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
}
}
override fun start(port: Int, props: Props?): Int? = null
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
override 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 =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}
}

View File

@@ -1,5 +1,6 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@@ -15,6 +16,7 @@ import android.os.Parcel
import android.os.RemoteException 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.BaseServiceInterface
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity import com.follow.clash.MainActivity
import com.follow.clash.R import com.follow.clash.R
@@ -25,10 +27,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class FlClashVpnService : VpnService() { @SuppressLint("WrongConstant")
private val CHANNEL = "FlClash" class FlClashVpnService : VpnService(), BaseServiceInterface {
private val notificationId: Int = 1
private val passList = listOf( private val passList = listOf(
"*zhihu.com", "*zhihu.com",
@@ -52,10 +52,10 @@ class FlClashVpnService : VpnService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initServiceEngine() GlobalState.initServiceEngine(applicationContext)
} }
fun start(port: Int, props: Props?): Int? { override fun start(port: Int, props: Props?): Int? {
return with(Builder()) { return with(Builder()) {
addAddress("172.16.0.1", 30) addAddress("172.16.0.1", 30)
setMtu(9000) setMtu(9000)
@@ -97,11 +97,18 @@ class FlClashVpnService : VpnService() {
} }
} }
fun stop() {
override fun stop() {
stopSelf() stopSelf()
stopForeground() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
} }
private val CHANNEL = "FlClash"
private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy { private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
@@ -136,16 +143,8 @@ class FlClashVpnService : VpnService() {
} }
} }
fun initServiceEngine() { @SuppressLint("ForegroundServiceType", "WrongConstant")
GlobalState.initServiceEngine(applicationContext) override fun startForeground(title: String, content: String) {
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentAppPlugin()?.requestGc()
}
fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL) var channel = manager?.getNotificationChannel(CHANNEL)
@@ -157,17 +156,16 @@ class FlClashVpnService : VpnService() {
} }
val notification = val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build() notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else { } else {
startForeground(notificationId, notification) startForeground(notificationId, notification)
} }
} }
private fun stopForeground() { override fun onTrimMemory(level: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { super.onTrimMemory(level)
stopForeground(STOP_FOREGROUND_REMOVE) GlobalState.getCurrentAppPlugin()?.requestGc()
}
} }
private val binder = LocalBinder() private val binder = LocalBinder()
@@ -190,7 +188,6 @@ class FlClashVpnService : VpnService() {
} }
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
return binder return binder
} }

Binary file not shown.

View File

@@ -3,6 +3,17 @@ package main
import "C" import "C"
import ( import (
"context" "context"
"errors"
"math"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -10,6 +21,7 @@ import (
"github.com/metacubex/mihomo/common/batch" "github.com/metacubex/mihomo/common/batch"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/sniffer"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider" cp "github.com/metacubex/mihomo/constant/provider"
@@ -20,51 +32,8 @@ import (
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider" rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
) )
//type healthCheckSchema struct {
// Enable bool `provider:"enable"`
// URL string `provider:"url"`
// Interval int `provider:"interval"`
// TestTimeout int `provider:"timeout,omitempty"`
// Lazy bool `provider:"lazy,omitempty"`
// ExpectedStatus string `provider:"expected-status,omitempty"`
//}
//type proxyProviderSchema struct {
// Type string `provider:"type"`
// Path string `provider:"path,omitempty"`
// URL string `provider:"url,omitempty"`
// Proxy string `provider:"proxy,omitempty"`
// Interval int `provider:"interval,omitempty"`
// Filter string `provider:"filter,omitempty"`
// ExcludeFilter string `provider:"exclude-filter,omitempty"`
// ExcludeType string `provider:"exclude-type,omitempty"`
// DialerProxy string `provider:"dialer-proxy,omitempty"`
//
// HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
// Override ap.OverrideSchema `provider:"override,omitempty"`
// Header map[string][]string `provider:"header,omitempty"`
//}
//
//type ruleProviderSchema struct {
// Type string `provider:"type"`
// Behavior string `provider:"behavior"`
// Path string `provider:"path,omitempty"`
// URL string `provider:"url,omitempty"`
// Proxy string `provider:"proxy,omitempty"`
// Format string `provider:"format,omitempty"`
// Interval int `provider:"interval,omitempty"`
//}
type ConfigExtendedParams struct { type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"` IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"` IsCompatible bool `json:"is-compatible"`
@@ -102,6 +71,12 @@ type ExternalProvider struct {
UpdateAt time.Time `json:"update-at"` UpdateAt time.Time `json:"update-at"`
} }
type ExternalProviders []ExternalProvider
func (a ExternalProviders) Len() int { return len(a) }
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
func restartExecutable(execPath string) { func restartExecutable(execPath string) {
@@ -190,35 +165,67 @@ func getRawConfigWithId(id string) *config.RawConfig {
return prof return prof
} }
func getExternalProvidersRaw() map[string]ExternalProvider { func getExternalProvidersRaw() map[string]cp.Provider {
externalProviders := make(map[string]ExternalProvider) eps := make(map[string]cp.Provider)
for n, p := range tunnel.Providers() { for n, p := range tunnel.Providers() {
if p.VehicleType() != cp.Compatible { if p.VehicleType() != cp.Compatible {
p := p.(*provider.ProxySetProvider) eps[n] = p
externalProviders[n] = ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
Count: p.Count(),
Path: p.Vehicle().Path(),
UpdateAt: p.UpdatedAt,
}
} }
} }
for n, p := range tunnel.RuleProviders() { for n, p := range tunnel.RuleProviders() {
if p.VehicleType() != cp.Compatible { if p.VehicleType() != cp.Compatible {
p := p.(*rp.RuleSetProvider) eps[n] = p
externalProviders[n] = ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
Count: p.Count(),
Path: p.Vehicle().Path(),
UpdateAt: p.UpdatedAt,
}
} }
} }
return externalProviders return eps
}
func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
switch p.(type) {
case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider)
return &ExternalProvider{
Name: psp.Name(),
Type: psp.Type().String(),
VehicleType: psp.VehicleType().String(),
Count: psp.Count(),
Path: psp.Vehicle().Path(),
UpdateAt: psp.UpdatedAt,
}, nil
case *rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
return &ExternalProvider{
Name: rsp.Name(),
Type: rsp.Type().String(),
VehicleType: rsp.VehicleType().String(),
Count: rsp.Count(),
Path: rsp.Vehicle().Path(),
UpdateAt: rsp.UpdatedAt,
}, nil
default:
return nil, errors.New("not external provider")
}
}
func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
switch p.(type) {
case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider)
elm, same, err := psp.SideUpdate(bytes)
if err == nil && !same {
psp.OnUpdate(elm)
}
return nil
case rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
elm, same, err := rsp.SideUpdate(bytes)
if err == nil && !same {
rsp.OnUpdate(elm)
}
return nil
default:
return errors.New("not external provider")
}
} }
func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig { func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig {
@@ -416,30 +423,65 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
func patchConfig(general *config.General) { func patchConfig(general *config.General) {
log.Infoln("[Apply] patch") log.Infoln("[Apply] patch")
route.ReStartServer(general.ExternalController) route.ReStartServer(general.ExternalController)
if sniffer.Dispatcher != nil {
tunnel.SetSniffing(general.Sniffing)
}
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay)
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6
}
var isRunning = false
var runLock sync.Mutex
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan) listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
inbound.SetAllowedIPs(general.LanAllowedIPs) inbound.SetAllowedIPs(general.LanAllowedIPs)
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs) inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
listener.SetBindAddress(general.BindAddress) listener.SetBindAddress(general.BindAddress)
tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay)
listener.ReCreateHTTP(general.Port, tunnel.Tunnel) listener.ReCreateHTTP(general.Port, tunnel.Tunnel)
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel) listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel) listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel) listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel)
listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel) listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel)
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel) listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel) listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel) listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel) listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
tunnel.SetMode(general.Mode) listener.ReCreateTun(general.Tun, tunnel.Tunnel)
log.SetLevel(general.LogLevel) listener.ReCreateRedirToTun(general.EBpf.RedirectToTun)
}
func stopListeners() {
listener.StopListener()
}
func hcCompatibleProvider(proxyProviders map[string]cp.ProxyProvider) {
wg := sync.WaitGroup{}
ch := make(chan struct{}, math.MaxInt)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == cp.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
}
}()
}
}
resolver.DisableIPv6 = !general.IPv6
} }
func patchSelectGroup() { func patchSelectGroup() {
@@ -467,12 +509,8 @@ func patchSelectGroup() {
} }
} }
var applyLock sync.Mutex
func applyConfig() error { func applyConfig() error {
applyLock.Lock() cfg, err := config.ParseRawConfig(currentRawConfig)
defer applyLock.Unlock()
cfg, err := config.ParseRawConfig(currentConfig)
if err != nil { if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig()) cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
} }
@@ -484,8 +522,13 @@ func applyConfig() error {
} else { } else {
closeConnections() closeConnections()
runtime.GC() runtime.GC()
hub.UltraApplyConfig(cfg, true) hub.UltraApplyConfig(cfg)
patchSelectGroup() patchSelectGroup()
} }
if isRunning {
updateListeners(cfg.General, cfg.Listeners)
hcCompatibleProvider(cfg.Providers)
}
externalProviders = getExternalProvidersRaw()
return err return err
} }

View File

@@ -8,6 +8,13 @@ import (
bridge "core/dart-bridge" bridge "core/dart-bridge"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"runtime"
"sort"
"sync"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/adapter/provider"
@@ -18,22 +25,34 @@ import (
cp "github.com/metacubex/mihomo/constant/provider" cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic" "github.com/metacubex/mihomo/tunnel/statistic"
"golang.org/x/net/context" "golang.org/x/net/context"
"os"
"runtime"
"time"
"unsafe"
) )
var currentConfig = config.DefaultRawConfig() var currentRawConfig = config.DefaultRawConfig()
var configParams = ConfigExtendedParams{} var configParams = ConfigExtendedParams{}
var externalProviders = map[string]cp.Provider{}
var isInit = false var isInit = false
//export start
func start() {
runLock.Lock()
defer runLock.Unlock()
isRunning = true
}
//export stop
func stop() {
runLock.Lock()
defer runLock.Unlock()
isRunning = false
stopListeners()
}
//export initClash //export initClash
func initClash(homeDirStr *C.char) bool { func initClash(homeDirStr *C.char) bool {
if !isInit { if !isInit {
@@ -57,10 +76,10 @@ func restartClash() bool {
//export shutdownClash //export shutdownClash
func shutdownClash() bool { func shutdownClash() bool {
stopListeners()
executor.Shutdown() executor.Shutdown()
runtime.GC() runtime.GC()
isInit = false isInit = false
currentConfig = nil
return true return true
} }
@@ -86,11 +105,15 @@ func validateConfig(s *C.char, port C.longlong) {
}() }()
} }
var updateLock sync.Mutex
//export updateConfig //export updateConfig
func updateConfig(s *C.char, port C.longlong) { func updateConfig(s *C.char, port C.longlong) {
i := int64(port) i := int64(port)
paramsString := C.GoString(s) paramsString := C.GoString(s)
go func() { go func() {
updateLock.Lock()
defer updateLock.Unlock()
var params = &GenerateConfigParams{} var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params) err := json.Unmarshal([]byte(paramsString), params)
if err != nil { if err != nil {
@@ -99,7 +122,7 @@ func updateConfig(s *C.char, port C.longlong) {
} }
configParams = params.Params configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config) prof := decorationConfig(params.ProfileId, params.Config)
currentConfig = prof currentRawConfig = prof
err = applyConfig() err = applyConfig()
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
@@ -311,34 +334,16 @@ func getProvider(name *C.char) *C.char {
//export getExternalProviders //export getExternalProviders
func getExternalProviders() *C.char { func getExternalProviders() *C.char {
externalProviders := make(map[string]ExternalProvider) eps := make([]ExternalProvider, 0)
for n, p := range tunnel.Providers() { for _, p := range externalProviders {
if p.VehicleType() != cp.Compatible { externalProvider, err := toExternalProvider(p)
p := p.(*provider.ProxySetProvider) if err != nil {
externalProviders[n] = ExternalProvider{ continue
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
Count: p.Count(),
Path: p.Vehicle().Path(),
UpdateAt: p.UpdatedAt,
}
} }
eps = append(eps, *externalProvider)
} }
for n, p := range tunnel.RuleProviders() { sort.Sort(ExternalProviders(eps))
if p.VehicleType() != cp.Compatible { data, err := json.Marshal(eps)
p := p.(*rp.RuleSetProvider)
externalProviders[n] = ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
Count: p.Count(),
Path: p.Vehicle().Path(),
UpdateAt: p.UpdatedAt,
}
}
}
data, err := json.Marshal(externalProviders)
if err != nil { if err != nil {
return C.CString("") return C.CString("")
} }
@@ -348,69 +353,48 @@ func getExternalProviders() *C.char {
//export getExternalProvider //export getExternalProvider
func getExternalProvider(name *C.char) *C.char { func getExternalProvider(name *C.char) *C.char {
externalProviderName := C.GoString(name) externalProviderName := C.GoString(name)
externalProviders := getExternalProvidersRaw()
externalProvider, exist := externalProviders[externalProviderName] externalProvider, exist := externalProviders[externalProviderName]
if !exist { if !exist {
return C.CString("") return C.CString("")
} }
data, err := json.Marshal(externalProvider) e, err := toExternalProvider(externalProvider)
if err != nil {
return C.CString("")
}
data, err := json.Marshal(e)
if err != nil { if err != nil {
return C.CString("") return C.CString("")
} }
return C.CString(string(data)) return C.CString(string(data))
} }
//export updateExternalProvider //export updateGeoData
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) { func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
i := int64(port) i := int64(port)
providerNameString := C.GoString(providerName) geoTypeString := C.GoString(geoType)
providerTypeString := C.GoString(providerType) geoNameString := C.GoString(geoName)
go func() { go func() {
switch providerTypeString { switch geoTypeString {
case "Proxy":
providers := tunnel.Providers()
proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider)
if !exist {
bridge.SendToPort(i, "proxy provider is not exist")
return
}
err := proxyProvider.Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "Rule":
providers := tunnel.RuleProviders()
ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider)
if !exist {
bridge.SendToPort(i, "rule provider is not exist")
return
}
err := ruleProvider.Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "MMDB": case "MMDB":
err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString)) err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "ASN": case "ASN":
err := updater.UpdateASN(constant.Path.Resolve(providerNameString)) err := updater.UpdateASN(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoIp": case "GeoIp":
err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString)) err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoSite": case "GeoSite":
err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString)) err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
@@ -420,65 +404,44 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l
}() }()
} }
//func sideLoadExternalProvider(providerName *C.char, providerType *C.char, data *C.char, port C.longlong) { //export updateExternalProvider
// i := int64(port) func updateExternalProvider(providerName *C.char, port C.longlong) {
// bytes := []byte(C.GoString(data)) i := int64(port)
// providerNameString := C.GoString(providerName) providerNameString := C.GoString(providerName)
// providerTypeString := C.GoString(providerType) go func() {
// go func() { externalProvider, exist := externalProviders[providerNameString]
// switch providerTypeString { if !exist {
// case "Proxy": bridge.SendToPort(i, "external provider is not exist")
// providers := tunnel.Providers() return
// proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider) }
// if exist { err := externalProvider.Update()
// bridge.SendToPort(i, "proxy provider is not exist") if err != nil {
// return bridge.SendToPort(i, err.Error())
// } return
// err := proxyProvider.Update() }
// if err != nil { bridge.SendToPort(i, "")
// bridge.SendToPort(i, err.Error()) }()
// return }
// }
// case "Rule": //export sideLoadExternalProvider
// providers := tunnel.RuleProviders() func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) {
// ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider) i := int64(port)
// if exist { bytes := []byte(C.GoString(data))
// bridge.SendToPort(i, "proxy provider is not exist") providerNameString := C.GoString(providerName)
// return go func() {
// } externalProvider, exist := externalProviders[providerNameString]
// err := ruleProvider.Update() if !exist {
// if err != nil { bridge.SendToPort(i, "external provider is not exist")
// bridge.SendToPort(i, err.Error()) return
// return }
// } err := sideUpdateExternalProvider(externalProvider, bytes)
// case "MMDB": if err != nil {
// err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString)) bridge.SendToPort(i, err.Error())
// if err != nil { return
// bridge.SendToPort(i, err.Error()) }
// return bridge.SendToPort(i, "")
// } }()
// case "ASN": }
// err := updater.UpdateASN(constant.Path.Resolve(providerNameString))
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// case "GeoIp":
// err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString))
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// case "GeoSite":
// err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString))
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// }
// bridge.SendToPort(i, "")
// }()
//}
//export initNativeApiBridge //export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) { func initNativeApiBridge(api unsafe.Pointer) {

View File

@@ -14,6 +14,7 @@ type AccessControl struct {
} }
type AndroidProps struct { type AndroidProps struct {
Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"` AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"` AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"` SystemProxy bool `json:"systemProxy"`

View File

@@ -7,14 +7,15 @@ import (
"core/platform" "core/platform"
t "core/tun" t "core/tun"
"errors" "errors"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"strconv" "strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
) )
var tunLock sync.Mutex var tunLock sync.Mutex
@@ -40,6 +41,18 @@ var fdMap FdMap
func startTUN(fd C.int, port C.longlong) { func startTUN(fd C.int, port C.longlong) {
i := int64(port) i := int64(port)
ServicePort = i ServicePort = i
if fd == 0 {
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
return
}
initSocketHook()
go func() { go func() {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
@@ -88,6 +101,7 @@ func getRunTime() *C.char {
//export stopTun //export stopTun
func stopTun() { func stopTun() {
removeSocketHook()
go func() { go func() {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
@@ -95,6 +109,7 @@ func stopTun() {
runTime = nil runTime = nil
if tun != nil { if tun != nil {
log.Errorln("[Tun] stopTun")
tun.Close() tun.Close()
tun = nil tun = nil
} }
@@ -125,7 +140,7 @@ func markSocket(fd Fd) {
var fdCounter int64 = 0 var fdCounter int64 = 0
func init() { func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() { if platform.ShouldBlockConnection() {
return errBlocked return errBlocked
@@ -159,3 +174,7 @@ func init() {
}) })
} }
} }
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}

View File

@@ -4,6 +4,7 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/proxy_container.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_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@@ -88,7 +89,6 @@ class ApplicationState extends State<Application> {
} }
await globalState.appController.init(); await globalState.appController.init();
globalState.appController.initLink(); globalState.appController.initLink();
_updateGroups();
}); });
} }
@@ -96,7 +96,9 @@ class ApplicationState extends State<Application> {
if (system.isDesktop) { if (system.isDesktop) {
return WindowContainer( return WindowContainer(
child: TrayContainer( child: TrayContainer(
child: app, child: ProxyContainer(
child: app,
),
), ),
); );
} }
@@ -120,75 +122,67 @@ class ApplicationState extends State<Application> {
}); });
} }
_updateGroups() {
if (globalState.groupsUpdateTimer != null) {
globalState.groupsUpdateTimer?.cancel();
globalState.groupsUpdateTimer = null;
}
globalState.groupsUpdateTimer ??= Timer.periodic(
httpTimeoutDuration,
(timer) async {
await globalState.appController.updateGroupDebounce();
},
);
}
@override @override
Widget build(context) { Widget build(context) {
return AppStateContainer( return _buildApp(
child: ClashContainer( AppStateContainer(
child: Selector2<AppState, Config, ApplicationSelectorState>( child: ClashContainer(
selector: (_, appState, config) => ApplicationSelectorState( child: Selector2<AppState, Config, ApplicationSelectorState>(
locale: config.locale, selector: (_, appState, config) => ApplicationSelectorState(
themeMode: config.themeMode, locale: config.locale,
primaryColor: config.primaryColor, themeMode: config.themeMode,
prueBlack: config.prueBlack, primaryColor: config.primaryColor,
), prueBlack: config.prueBlack,
builder: (_, state, child) { ),
return DynamicColorBuilder( builder: (_, state, child) {
builder: (lightDynamic, darkDynamic) { return DynamicColorBuilder(
_updateSystemColorSchemes(lightDynamic, darkDynamic); builder: (lightDynamic, darkDynamic) {
return MaterialApp( _updateSystemColorSchemes(lightDynamic, darkDynamic);
debugShowCheckedModeBanner: false, return MaterialApp(
navigatorKey: globalState.navigatorKey, navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate GlobalWidgetsLocalizations.delegate
], ],
builder: (_, child) { builder: (_, child) {
return _buildApp(child!); if (system.isDesktop) {
}, return WindowHeaderContainer(child: child!);
scrollBehavior: BaseScrollBehavior(), }
title: appName, return child!;
locale: other.getLocaleForString(state.locale), },
supportedLocales: AppLocalizations.delegate.supportedLocales, scrollBehavior: BaseScrollBehavior(),
themeMode: state.themeMode, title: appName,
theme: ThemeData( locale: other.getLocaleForString(state.locale),
useMaterial3: true, supportedLocales:
pageTransitionsTheme: _pageTransitionsTheme, AppLocalizations.delegate.supportedLocales,
colorScheme: _getAppColorScheme( themeMode: state.themeMode,
brightness: Brightness.light, theme: ThemeData(
systemColorSchemes: systemColorSchemes, useMaterial3: true,
primaryColor: state.primaryColor, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
), ),
), darkTheme: ThemeData(
darkTheme: ThemeData( useMaterial3: true,
useMaterial3: true, pageTransitionsTheme: _pageTransitionsTheme,
pageTransitionsTheme: _pageTransitionsTheme, colorScheme: _getAppColorScheme(
colorScheme: _getAppColorScheme( brightness: Brightness.dark,
brightness: Brightness.dark, systemColorSchemes: systemColorSchemes,
systemColorSchemes: systemColorSchemes, primaryColor: state.primaryColor,
primaryColor: state.primaryColor, ).toPrueBlack(state.prueBlack),
).toPrueBlack(state.prueBlack), ),
), home: child,
home: child, );
); },
}, );
); },
}, child: const HomePage(),
child: const HomePage(), ),
), ),
), ),
); );

View File

@@ -140,8 +140,7 @@ class ClashCore {
clashFFI.freeCString(externalProvidersRaw); clashFFI.freeCString(externalProvidersRaw);
return Isolate.run<List<ExternalProvider>>(() { return Isolate.run<List<ExternalProvider>>(() {
final externalProviders = final externalProviders =
(json.decode(externalProvidersRawString) as Map<String, dynamic>) (json.decode(externalProvidersRawString) as List<dynamic>)
.values
.map( .map(
(item) => ExternalProvider.fromJson(item), (item) => ExternalProvider.fromJson(item),
) )
@@ -150,7 +149,7 @@ class ClashCore {
}); });
} }
ExternalProvider getExternalProvider(String externalProviderName) { ExternalProvider? getExternalProvider(String externalProviderName) {
final externalProviderNameChar = final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>(); externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw = final externalProviderRaw =
@@ -159,12 +158,37 @@ class ClashCore {
final externalProviderRawString = final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString(); externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw); clashFFI.freeCString(externalProviderRaw);
if(externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString)); return ExternalProvider.fromJson(json.decode(externalProviderRawString));
} }
Future<String> updateExternalProvider({ Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
clashFFI.updateGeoData(
geoTypeChar,
geoNameChar,
receiver.sendPort.nativePort,
);
malloc.free(geoTypeChar);
malloc.free(geoNameChar);
return completer.future;
}
Future<String> sideLoadExternalProvider({
required String providerName, required String providerName,
required String providerType, required String data,
}) { }) {
final completer = Completer<String>(); final completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
@@ -175,14 +199,34 @@ class ClashCore {
} }
}); });
final providerNameChar = providerName.toNativeUtf8().cast<Char>(); final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final providerTypeChar = providerType.toNativeUtf8().cast<Char>(); final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider( clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
}
Future<String> updateExternalProvider({
required String providerName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar, providerNameChar,
providerTypeChar,
receiver.sendPort.nativePort, receiver.sendPort.nativePort,
); );
malloc.free(providerNameChar); malloc.free(providerNameChar);
malloc.free(providerTypeChar);
return completer.future; return completer.future;
} }
@@ -193,6 +237,14 @@ class ClashCore {
malloc.free(paramsChar); malloc.free(paramsChar);
} }
start() {
clashFFI.start();
}
stop() {
clashFFI.stop();
}
Future<Delay> getDelay(String proxyName) { Future<Delay> getDelay(String proxyName) {
final delayParams = { final delayParams = {
"proxy-name": proxyName, "proxy-name": proxyName,

View File

@@ -5144,6 +5144,22 @@ class ClashFFI {
late final __FCmulcr = late final __FCmulcr =
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>(); __FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
void start() {
return _start();
}
late final _startPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('start');
late final _start = _startPtr.asFunction<void Function()>();
void stop() {
return _stop();
}
late final _stopPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stop');
late final _stop = _stopPtr.asFunction<void Function()>();
int initClash( int initClash(
ffi.Pointer<ffi.Char> homeDirStr, ffi.Pointer<ffi.Char> homeDirStr,
) { ) {
@@ -5400,24 +5416,61 @@ class ClashFFI {
late final _getExternalProvider = _getExternalProviderPtr late final _getExternalProvider = _getExternalProviderPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>(); .asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void updateGeoData(
ffi.Pointer<ffi.Char> geoType,
ffi.Pointer<ffi.Char> geoName,
int port,
) {
return _updateGeoData(
geoType,
geoName,
port,
);
}
late final _updateGeoDataPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateGeoData');
late final _updateGeoData = _updateGeoDataPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void updateExternalProvider( void updateExternalProvider(
ffi.Pointer<ffi.Char> providerName, ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> providerType,
int port, int port,
) { ) {
return _updateExternalProvider( return _updateExternalProvider(
providerName, providerName,
providerType,
port, port,
); );
} }
late final _updateExternalProviderPtr = _lookup< late final _updateExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void sideLoadExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> data,
int port,
) {
return _sideLoadExternalProvider(
providerName,
data,
port,
);
}
late final _sideLoadExternalProviderPtr = _lookup<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateExternalProvider'); ffi.LongLong)>>('sideLoadExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr.asFunction< late final _sideLoadExternalProvider =
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>(); _sideLoadExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void initNativeApiBridge( void initNativeApiBridge(
ffi.Pointer<ffi.Void> api, ffi.Pointer<ffi.Void> api,

View File

@@ -23,6 +23,6 @@ export 'app_localizations.dart';
export 'function.dart'; export 'function.dart';
export 'package.dart'; export 'package.dart';
export 'measure.dart'; export 'measure.dart';
export 'service.dart'; export 'windows.dart';
export 'iterable.dart'; export 'iterable.dart';
export 'scroll.dart'; export 'scroll.dart';

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';

View File

@@ -1,12 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
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/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:path/path.dart';
import 'package:webdav_client/webdav_client.dart'; import 'package:webdav_client/webdav_client.dart';
class DAVClient { class DAVClient {
@@ -34,8 +30,6 @@ class DAVClient {
Future<bool> _ping() async { Future<bool> _ping() async {
try { try {
await client.ping(); await client.ping();
await client.mkdir("/$appName");
await client.mkdir("/$appName/$profilesDirectoryName");
return true; return true;
} catch (_) { } catch (_) {
return false; return false;
@@ -53,6 +47,7 @@ class DAVClient {
} }
Future<List<int>> recovery() async { Future<List<int>> recovery() async {
await client.mkdir("$root");
final data = await client.read(backupFile); final data = await client.read(backupFile);
return data; return data;
} }

View File

@@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/models/models.dart' hide Process;
import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart'; import 'constant.dart';
import 'system.dart'; import 'system.dart';
import 'windows.dart';
class AutoLaunch { class AutoLaunch {
static AutoLaunch? _instance; static AutoLaunch? _instance;
@@ -24,18 +26,77 @@ class AutoLaunch {
return await launchAtStartup.isEnabled(); return await launchAtStartup.isEnabled();
} }
Future<bool> get windowsIsEnable async {
final res = await Process.run(
'schtasks',
['/Query', '/TN', appName, '/V', "/FO", "LIST"],
runInShell: true,
);
return res.stdout.toString().contains(Platform.resolvedExecutable);
}
Future<bool> enable() async { Future<bool> enable() async {
if (Platform.isWindows) {
await windowsDisable();
}
return await launchAtStartup.enable(); return await launchAtStartup.enable();
} }
windowsDisable() async {
final res = await Process.run(
'schtasks',
[
'/Delete',
'/TN',
appName,
'/F',
],
runInShell: true,
);
return res.exitCode == 0;
}
Future<bool> windowsEnable() async {
await disable();
return windows?.runas(
'schtasks',
[
'/Create',
'/SC',
'ONLOGON',
'/TN',
appName,
'/TR',
Platform.resolvedExecutable,
'/RL',
'HIGHEST',
'/F'
].join(" "),
) ??
false;
}
Future<bool> disable() async { Future<bool> disable() async {
return await launchAtStartup.disable(); return await launchAtStartup.disable();
} }
updateStatus(bool value) async { updateStatus(AutoLaunchState state) async {
final isEnable = await this.isEnable; final isOpenTun = state.isOpenTun;
if (isEnable == value) return; final isAutoLaunch = state.isAutoLaunch;
if (value == true) { if (Platform.isWindows && isOpenTun) {
if (await windowsIsEnable == isAutoLaunch) return;
if (isAutoLaunch) {
final isEnable = await windowsEnable();
if (!isEnable) {
enable();
}
} else {
windowsDisable();
}
return;
}
if (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) {
enable(); enable();
} else { } else {
disable(); disable();

View File

@@ -1,10 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/constant.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:zxing2/qrcode.dart'; import 'package:zxing2/qrcode.dart';
@@ -84,7 +83,7 @@ class Other {
if (charA == charB) { if (charA == charB) {
return sortByChar(a.substring(1), b.substring(1)); return sortByChar(a.substring(1), b.substring(1));
} else { } else {
return charA.compareTo(charB); return charA.compareToLower(charB);
} }
} }
@@ -192,25 +191,20 @@ class Other {
return ViewMode.desktop; return ViewMode.desktop;
} }
int getColumns(ViewMode viewMode, int currentColumns) { int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
final targetColumnsArray = viewModeColumnsMap[viewMode]!; final columns = max((viewWidth / 300).ceil(), 2);
if (targetColumnsArray.contains(currentColumns)) { return switch (proxiesLayout) {
return currentColumns; ProxiesLayout.tight => columns - 1,
} ProxiesLayout.standard => columns,
return targetColumnsArray.first; ProxiesLayout.loose => columns + 1,
}
String getColumnsTextForInt(int number){
return switch(number){
1 => appLocalizations.oneColumn,
2 => appLocalizations.twoColumns,
3 => appLocalizations.threeColumns,
4 => appLocalizations.fourColumns,
int() => throw UnimplementedError(),
}; };
} }
String getBackupFileName(){ int getProfilesColumns(double viewWidth) {
return max((viewWidth / 400).floor(), 1);
}
String getBackupFileName() {
return "${appName}_backup_${DateTime.now().show}.zip"; return "${appName}_backup_${DateTime.now().show}.zip";
} }
} }

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@@ -14,12 +15,16 @@ class Picker {
return filePickerResult?.files.first; return filePickerResult?.files.first;
} }
Future<String?> saveFile(String fileName,Uint8List bytes) async { Future<String?> saveFile(String fileName, Uint8List bytes) async {
final path = await FilePicker.platform.saveFile( final path = await FilePicker.platform.saveFile(
fileName: fileName, fileName: fileName,
initialDirectory: await appPath.getDownloadDirPath(), initialDirectory: await appPath.getDownloadDirPath(),
bytes: bytes, bytes: Platform.isAndroid ? bytes : null,
); );
if (!Platform.isAndroid && path != null) {
final file = await File(path).create(recursive: true);
await file.writeAsBytes(bytes);
}
return path; return path;
} }

View File

@@ -1,37 +1,4 @@
import 'package:fl_clash/common/datetime.dart'; import 'package:fl_clash/common/system.dart';
import 'package:fl_clash/plugins/proxy.dart'; import 'package:proxy/proxy.dart';
import 'package:proxy/proxy.dart' as proxy_plugin;
import 'package:proxy/proxy_platform_interface.dart';
class ProxyManager { final proxy = system.isDesktop ? Proxy() : null;
static ProxyManager? _instance;
late ProxyPlatform _proxy;
ProxyManager._internal() {
_proxy = proxy ?? proxy_plugin.Proxy();
}
bool get isStart => startTime != null && startTime!.isBeforeNow;
DateTime? get startTime => _proxy.startTime;
Future<bool?> startProxy({required int port}) async {
return await _proxy.startProxy(port);
}
Future<bool?> stopProxy() async {
return await _proxy.stopProxy();
}
Future<DateTime?> updateStartTime() async {
if (_proxy is! Proxy) return null;
return await (_proxy as Proxy).updateStartTime();
}
factory ProxyManager() {
_instance ??= ProxyManager._internal();
return _instance!;
}
}
final proxyManager = ProxyManager();

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
@@ -13,9 +14,6 @@ class Request {
Request() { Request() {
_dio = Dio(); _dio = Dio();
_dio.options = BaseOptions(
headers: {"User-Agent": globalState.appController.clashConfig.globalUa},
);
_dio.interceptors.add( _dio.interceptors.add(
InterceptorsWrapper( InterceptorsWrapper(
onRequest: (options, handler) { onRequest: (options, handler) {
@@ -52,11 +50,14 @@ class Request {
.get( .get(
url, url,
options: Options( options: Options(
headers: {
"User-Agent": globalState.appController.clashConfig.globalUa
},
responseType: ResponseType.bytes, responseType: ResponseType.bytes,
), ),
) )
.timeout( .timeout(
httpTimeoutDuration * 2, httpTimeoutDuration * 6,
); );
return response; return response;
} }
@@ -86,10 +87,13 @@ class Request {
}; };
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async { Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) { for (final source in _ipInfoSources.entries.toList()..shuffle(Random())) {
try { try {
final response = await _dio final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken) .get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
)
.timeout( .timeout(
httpTimeoutDuration, httpTimeoutDuration,
); );
@@ -97,6 +101,9 @@ class Request {
return source.value(response.data!); return source.value(response.data!);
} }
} catch (e) { } catch (e) {
if(cancelToken?.isCancelled == true){
throw "cancelled";
}
continue; continue;
} }
} }

View File

@@ -1,110 +0,0 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
typedef CreateServiceNative = IntPtr Function(
IntPtr hSCManager,
Pointer<Utf16> lpServiceName,
Pointer<Utf16> lpDisplayName,
Uint32 dwDesiredAccess,
Uint32 dwServiceType,
Uint32 dwStartType,
Uint32 dwErrorControl,
Pointer<Utf16> lpBinaryPathName,
Pointer<Utf16> lpLoadOrderGroup,
Pointer<Uint32> lpdwTagId,
Pointer<Utf16> lpDependencies,
Pointer<Utf16> lpServiceStartName,
Pointer<Utf16> lpPassword,
);
typedef CreateServiceDart = int Function(
int hSCManager,
Pointer<Utf16> lpServiceName,
Pointer<Utf16> lpDisplayName,
int dwDesiredAccess,
int dwServiceType,
int dwStartType,
int dwErrorControl,
Pointer<Utf16> lpBinaryPathName,
Pointer<Utf16> lpLoadOrderGroup,
Pointer<Uint32> lpdwTagId,
Pointer<Utf16> lpDependencies,
Pointer<Utf16> lpServiceStartName,
Pointer<Utf16> lpPassword,
);
const _SERVICE_ALL_ACCESS = 0xF003F;
const _SERVICE_WIN32_OWN_PROCESS = 0x00000010;
const _SERVICE_AUTO_START = 0x00000002;
const _SERVICE_ERROR_NORMAL = 0x00000001;
typedef GetLastErrorNative = Uint32 Function();
typedef GetLastErrorDart = int Function();
class Service {
static Service? _instance;
late DynamicLibrary _advapi32;
Service._internal() {
_advapi32 = DynamicLibrary.open('advapi32.dll');
}
factory Service() {
_instance ??= Service._internal();
return _instance!;
}
Future<void> createService() async {
final int scManager = OpenSCManager(nullptr, nullptr, _SERVICE_ALL_ACCESS);
if (scManager == 0) return;
final serviceName = 'FlClash Service'.toNativeUtf16();
final displayName = 'FlClash Service'.toNativeUtf16();
final binaryPathName = "C:\\Application\\Clash.Verge_1.6.6_x64_portable\\resources\\clash-verge-service.exe".toNativeUtf16();
final createService =
_advapi32.lookupFunction<CreateServiceNative, CreateServiceDart>(
'CreateServiceW',
);
final getLastError = DynamicLibrary.open('kernel32.dll')
.lookupFunction<GetLastErrorNative, GetLastErrorDart>('GetLastError');
final serviceHandle = createService(
scManager,
serviceName,
displayName,
_SERVICE_ALL_ACCESS,
_SERVICE_WIN32_OWN_PROCESS,
_SERVICE_AUTO_START,
_SERVICE_ERROR_NORMAL,
binaryPathName,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
);
print("serviceHandle $serviceHandle");
final errorCode = GetLastError();
print('Error code: $errorCode');
final result = StartService(serviceHandle, 0, nullptr);
if (result == 0) {
print('Failed to start the service.');
} else {
print('Service started successfully.');
}
calloc.free(serviceName);
calloc.free(displayName);
calloc.free(binaryPathName);
}
}
final service = Platform.isWindows ? Service() : null;

View File

@@ -2,4 +2,10 @@ extension StringExtension on String {
bool get isUrl { bool get isUrl {
return RegExp(r'^(http|https|ftp)://').hasMatch(this); return RegExp(r'^(http|https|ftp)://').hasMatch(this);
} }
int compareToLower(String other) {
return toLowerCase().compareTo(
other.toLowerCase(),
);
}
} }

View File

@@ -18,6 +18,12 @@ class System {
bool get isDesktop => bool get isDesktop =>
Platform.isWindows || Platform.isMacOS || Platform.isLinux; Platform.isWindows || Platform.isMacOS || Platform.isLinux;
get isAdmin async {
if (!Platform.isWindows) return false;
final result = await Process.run('net', ['session'], runInShell: true);
return result.exitCode == 0;
}
back() async { back() async {
await app?.moveTaskToBack(); await app?.moveTaskToBack();
await window?.hide(); await window?.hide();

View File

@@ -34,7 +34,7 @@ class Window {
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden); // await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
// } // }
await windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setPreventClose(true); // await windowManager.setPreventClose(true);
}); });
} }

59
lib/common/windows.dart Normal file
View File

@@ -0,0 +1,59 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
class Windows {
static Windows? _instance;
late DynamicLibrary _shell32;
Windows._internal() {
_shell32 = DynamicLibrary.open('shell32.dll');
}
factory Windows() {
_instance ??= Windows._internal();
return _instance!;
}
bool runas(String command, String arguments) {
final commandPtr = command.toNativeUtf16();
final argumentsPtr = arguments.toNativeUtf16();
final operationPtr = 'runas'.toNativeUtf16();
final shellExecute = _shell32.lookupFunction<
Int32 Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
Int32 nShowCmd),
int Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
int nShowCmd)>('ShellExecuteW');
final result = shellExecute(
nullptr,
operationPtr,
commandPtr,
argumentsPtr,
nullptr,
1,
);
calloc.free(commandPtr);
calloc.free(argumentsPtr);
calloc.free(operationPtr);
if (result <= 32) {
return false;
}
return true;
}
}
final windows = Platform.isWindows ? Windows() : null;

View File

@@ -8,6 +8,7 @@ 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/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -25,6 +26,7 @@ class AppController {
late Function updateClashConfigDebounce; late Function updateClashConfigDebounce;
late Function updateGroupDebounce; late Function updateGroupDebounce;
late Function addCheckIpNumDebounce; late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
AppController(this.context) { AppController(this.context) {
appState = context.read<AppState>(); appState = context.read<AppState>();
@@ -33,6 +35,9 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async { updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig(); await updateClashConfig();
}); });
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
addCheckIpNumDebounce = debounce(() { addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++; appState.checkIpNum++;
}); });
@@ -42,10 +47,9 @@ class AppController {
measure = Measure.of(context); measure = Measure.of(context);
} }
Future<void> updateSystemProxy(bool isStart) async { updateStatus(bool isStart) async {
if (isStart) { if (isStart) {
await globalState.startSystemProxy( await globalState.handleStart(
appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
@@ -55,10 +59,9 @@ class AppController {
updateRunTime, updateRunTime,
updateTraffic, updateTraffic,
]; ];
if (Platform.isAndroid) return; applyProfileDebounce();
await applyProfile(isPrue: true);
} else { } else {
await globalState.stopSystemProxy(); await globalState.handleStop();
clashCore.resetTraffic(); clashCore.resetTraffic();
appState.traffics = []; appState.traffics = [];
appState.totalTraffic = Traffic(); appState.totalTraffic = Traffic();
@@ -72,8 +75,9 @@ class AppController {
} }
updateRunTime() { updateRunTime() {
if (proxyManager.startTime != null) { final startTime = globalState.startTime;
final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch; if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch; final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appState.runTime = nowTimeStamp - startTimeStamp; appState.runTime = nowTimeStamp - startTimeStamp;
} else { } else {
@@ -101,7 +105,7 @@ class AppController {
final updateId = config.profiles.first.id; final updateId = config.profiles.first.id;
changeProfile(updateId); changeProfile(updateId);
} else { } else {
updateSystemProxy(false); updateStatus(false);
} }
} }
} }
@@ -227,7 +231,7 @@ class AppController {
} }
handleExit() async { handleExit() async {
await updateSystemProxy(false); await updateStatus(false);
await savePreferences(); await savePreferences();
clashCore.shutdown(); clashCore.shutdown();
system.exit(); system.exit();
@@ -296,11 +300,13 @@ class AppController {
if (!config.silentLaunch) { if (!config.silentLaunch) {
window?.show(); window?.show();
} }
await proxyManager.updateStartTime(); if (Platform.isAndroid) {
if (proxyManager.isStart) { globalState.updateStartTime();
await updateSystemProxy(true); }
if (globalState.isStart) {
await updateStatus(true);
} else { } else {
await updateSystemProxy(config.autoRun); await updateStatus(config.autoRun);
} }
autoUpdateProfiles(); autoUpdateProfiles();
autoCheckUpdate(); autoCheckUpdate();
@@ -364,6 +370,10 @@ class AppController {
); );
} }
showSnackBar(String message) {
globalState.showSnackBar(context, message: message);
}
addProfileFormURL(String url) async { addProfileFormURL(String url) async {
if (globalState.navigatorKey.currentState?.canPop() ?? false) { if (globalState.navigatorKey.currentState?.canPop() ?? false) {
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
@@ -413,8 +423,6 @@ class AppController {
addProfileFormURL(url); addProfileFormURL(url);
} }
int get columns => other.getColumns(appState.viewMode, config.proxiesColumns);
updateViewWidth(double width) { updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width; appState.viewWidth = width;
@@ -424,7 +432,10 @@ class AppController {
List<Proxy> _sortOfName(List<Proxy> proxies) { List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies) return List.of(proxies)
..sort( ..sort(
(a, b) => other.sortByChar(a.name, b.name), (a, b) => other.sortByChar(
PinyinHelper.getPinyin(a.name),
PinyinHelper.getPinyin(b.name),
),
); );
} }

View File

@@ -52,6 +52,8 @@ enum TunStack { gvisor, system, mixed }
enum AccessControlMode { acceptSelected, rejectSelected } enum AccessControlMode { acceptSelected, rejectSelected }
enum AccessSortType { none, name, time }
enum ProfileType { file, url } enum ProfileType { file, url }
enum ResultType { success, error } enum ResultType { success, error }
@@ -84,4 +86,6 @@ enum CommonCardType { plain, filled }
enum ProxiesType { tab, list } enum ProxiesType { tab, list }
enum ProxiesLayout{ loose, standard, tight }
enum ProxyCardType { expand, shrink, min } enum ProxyCardType { expand, shrink, min }

View File

@@ -1,4 +1,5 @@
import 'package:collection/collection.dart'; import 'dart:convert';
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/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
@@ -6,15 +7,9 @@ import 'package:fl_clash/common/common.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/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
extension AccessControlExtension on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
class AccessFragment extends StatefulWidget { class AccessFragment extends StatefulWidget {
const AccessFragment({super.key}); const AccessFragment({super.key});
@@ -23,9 +18,13 @@ class AccessFragment extends StatefulWidget {
} }
class _AccessFragmentState extends State<AccessFragment> { class _AccessFragmentState extends State<AccessFragment> {
List<String> acceptList = [];
List<String> rejectList = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateInitList();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState; final appState = globalState.appController.appState;
if (appState.packages.isEmpty) { if (appState.packages.isEmpty) {
@@ -36,297 +35,83 @@ class _AccessFragmentState extends State<AccessFragment> {
}); });
} }
Widget _buildAppProxyModePopup() { _updateInitList() {
final items = [ final accessControl = globalState.appController.config.accessControl;
CommonPopupMenuItem( acceptList = accessControl.acceptList;
action: AccessControlMode.rejectSelected, rejectList = accessControl.rejectList;
label: appLocalizations.blacklistMode,
),
CommonPopupMenuItem(
action: AccessControlMode.acceptSelected,
label: appLocalizations.whitelistMode,
),
];
return Selector<Config, AccessControlMode>(
selector: (_, config) => config.accessControl.mode,
builder: (context, mode, __) {
return CommonPopupMenu<AccessControlMode>.radio(
icon: Icon(
Icons.mode_standby,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
items: items,
onSelected: (value) {
final config = context.read<Config>();
config.accessControl = config.accessControl.copyWith(
mode: value,
);
},
selectedValue: mode,
);
},
);
} }
Widget _buildFilterSystemAppButton() { Widget _buildSearchButton() {
return Selector<Config, bool>(
selector: (_, config) => config.accessControl.isFilterSystemApp,
builder: (context, isFilterSystemApp, __) {
final tooltip = isFilterSystemApp
? appLocalizations.cancelFilterSystemApp
: appLocalizations.filterSystemApp;
return IconButton(
tooltip: tooltip,
onPressed: () {
final config = context.read<Config>();
config.accessControl = config.accessControl.copyWith(
isFilterSystemApp: !isFilterSystemApp,
);
},
icon: isFilterSystemApp
? const Icon(Icons.filter_list_off)
: const Icon(Icons.filter_list),
);
},
);
}
Widget _buildSearchButton(List<Package> packages) {
return IconButton( return IconButton(
tooltip: appLocalizations.search, tooltip: appLocalizations.search,
onPressed: () { onPressed: () {
showSearch( showSearch(
context: context, context: context,
delegate: AccessControlSearchDelegate( delegate: AccessControlSearchDelegate(
packages: packages, acceptList: acceptList,
rejectList: rejectList,
), ),
).then((_) => {setState(() {})}); ).then((_) => setState(() {
_updateInitList();
}));
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
); );
} }
// Widget _buildSelectedAllButton({ Widget _buildSelectedAllButton({
// required bool isAccessControl, required bool isSelectedAll,
// required bool isSelectedAll, required List<String> allValueList,
// required List<String> allValueList, }) {
// }) { final tooltip = isSelectedAll
// final tooltip = isSelectedAll ? appLocalizations.cancelSelectAll
// ? appLocalizations.cancelSelectAll : appLocalizations.selectAll;
// : appLocalizations.selectAll; return IconButton(
// return AbsorbPointer( tooltip: tooltip,
// absorbing: !isAccessControl, onPressed: () {
// child: FloatingActionButton( final config = globalState.appController.config;
// tooltip: tooltip, final isAccept =
// onPressed: () { config.accessControl.mode == AccessControlMode.acceptSelected;
// final config = globalState.appController.config; if (isSelectedAll) {
// final isAccept = config.accessControl = switch (isAccept) {
// config.accessControl.mode == AccessControlMode.acceptSelected; true => config.accessControl.copyWith(
// acceptList: [],
// if (isSelectedAll) {
// config.accessControl = switch (isAccept) {
// true => config.accessControl.copyWith(
// acceptList: [],
// ),
// false => config.accessControl.copyWith(
// rejectList: [],
// ),
// };
// } else {
// config.accessControl = switch (isAccept) {
// true => config.accessControl.copyWith(
// acceptList: allValueList,
// ),
// false => config.accessControl.copyWith(
// rejectList: allValueList,
// ),
// };
// }
// },
// child: isSelectedAll
// ? const Icon(Icons.deselect)
// : const Icon(Icons.select_all),
// ),
// );
// }
Widget _buildPackageList() {
return Selector<AppState, List<Package>>(
selector: (_, appState) => appState.packages,
builder: (_, packages, ___) {
final accessControl = globalState.appController.config.accessControl;
final acceptList = accessControl.acceptList;
final rejectList = accessControl.rejectList;
final acceptPackages = packages.sorted((a, b) {
final isSelectA = acceptList.contains(a.packageName);
final isSelectB = acceptList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
});
final rejectPackages = packages.sorted((a, b) {
final isSelectA = rejectList.contains(a.packageName);
final isSelectB = rejectList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
});
return Selector<Config, PackageListSelectorState>(
selector: (_, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final isFilterSystemApp = accessControl.isFilterSystemApp;
final accessControlMode = accessControl.mode;
final packages =
accessControlMode == AccessControlMode.acceptSelected
? acceptPackages
: rejectPackages;
final currentList = accessControl.currentList;
final currentPackages = isFilterSystemApp
? packages
.where((element) => element.isSystem == false)
.toList()
: packages;
final packageNameList =
currentPackages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
AbsorbPointer(
absorbing: !isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(currentPackages)),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
),
),
),
Expanded(
flex: 1,
child: currentPackages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: currentPackages.length,
itemBuilder: (_, index) {
final package = currentPackages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
),
],
), ),
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
icon: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
);
}
Widget _buildSettingButton() {
return IconButton(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
builder: (_) {
return AccessControlWidget(
context: context,
); );
}, },
); );
}, },
icon: const Icon(Icons.tune),
); );
} }
@@ -363,7 +148,170 @@ class _AccessFragmentState extends State<AccessFragment> {
], ],
); );
}, },
child: _buildPackageList(), child: Selector<AppState, List<Package>>(
selector: (_, appState) => appState.packages,
builder: (_, packages, ___) {
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
packages: appState.packages,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final currentList = accessControl.currentList;
final packageNameList =
packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
AbsorbPointer(
absorbing: !isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(),
),
Flexible(
child: _buildSelectedAllButton(
isSelectedAll: valueList.length ==
packageNameList.length,
allValueList: packageNameList,
),
),
Flexible(
child: _buildSettingButton(),
),
],
),
],
),
),
),
Expanded(
flex: 1,
child: packages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: packages.length,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value:
valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
),
],
),
);
},
);
},
),
); );
} }
} }
@@ -430,23 +378,14 @@ class PackageListItem extends StatelessWidget {
} }
class AccessControlSearchDelegate extends SearchDelegate { class AccessControlSearchDelegate extends SearchDelegate {
final List<Package> packages; List<String> acceptList = [];
List<String> rejectList = [];
AccessControlSearchDelegate({ AccessControlSearchDelegate({
required this.packages, required this.acceptList,
required this.rejectList,
}); });
List<Package> get _results {
final lowQuery = query.toLowerCase();
return packages
.where(
(package) =>
package.label.toLowerCase().contains(lowQuery) ||
package.packageName.contains(lowQuery),
)
.toList();
}
@override @override
List<Widget>? buildActions(BuildContext context) { List<Widget>? buildActions(BuildContext context) {
return [ return [
@@ -476,26 +415,39 @@ class AccessControlSearchDelegate extends SearchDelegate {
); );
} }
Widget _packageList(List<Package> packages) { Widget _packageList() {
return Selector<Config, PackageListSelectorState>( final lowQuery = query.toLowerCase();
selector: (_, config) => PackageListSelectorState( return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
packages: appState.packages,
accessControl: config.accessControl, accessControl: config.accessControl,
isAccessControl: config.isAccessControl, isAccessControl: config.isAccessControl,
), ),
builder: (context, state, __) { builder: (context, state, __) {
final accessControl = state.accessControl; final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode; final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final queryPackages = packages
.where(
(package) =>
package.label.toLowerCase().contains(lowQuery) ||
package.packageName.contains(lowQuery),
)
.toList();
final isAccessControl = state.isAccessControl;
final currentList = accessControl.currentList; final currentList = accessControl.currentList;
final packageNameList = final packageNameList = packages.map((e) => e.packageName).toList();
this.packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList); final valueList = currentList.intersection(packageNameList);
return DisabledMask( return DisabledMask(
status: !isAccessControl, status: !isAccessControl,
child: ListView.builder( child: ListView.builder(
itemCount: packages.length, itemCount: queryPackages.length,
itemBuilder: (_, index) { itemBuilder: (_, index) {
final package = packages[index]; final package = queryPackages[index];
return PackageListItem( return PackageListItem(
key: Key(package.packageName), key: Key(package.packageName),
package: package, package: package,
@@ -533,6 +485,268 @@ class AccessControlSearchDelegate extends SearchDelegate {
@override @override
Widget buildSuggestions(BuildContext context) { Widget buildSuggestions(BuildContext context) {
return _packageList(_results); return _packageList();
}
}
class AccessControlWidget extends StatelessWidget {
final BuildContext context;
const AccessControlWidget({
super.key,
required this.context,
});
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
return switch (mode) {
AccessControlMode.acceptSelected => Icons.adjust_outlined,
AccessControlMode.rejectSelected => Icons.block_outlined,
};
}
String _getTextWithAccessControlMode(AccessControlMode mode) {
return switch (mode) {
AccessControlMode.acceptSelected => appLocalizations.whitelistMode,
AccessControlMode.rejectSelected => appLocalizations.blacklistMode,
};
}
String _getTextWithAccessSortType(AccessSortType type) {
return switch (type) {
AccessSortType.none => appLocalizations.defaultText,
AccessSortType.name => appLocalizations.name,
AccessSortType.time => appLocalizations.time,
};
}
IconData _getIconWithProxiesSortType(AccessSortType type) {
return switch (type) {
AccessSortType.none => Icons.sort,
AccessSortType.name => Icons.sort_by_alpha,
AccessSortType.time => Icons.timeline,
};
}
String _getTextWithIsFilterSystemApp(bool isFilterSystemApp) {
return switch (isFilterSystemApp) {
true => appLocalizations.onlyOtherApps,
false => appLocalizations.allApps,
};
}
List<Widget> _buildModeSetting() {
return generateSection(
title: appLocalizations.mode,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, AccessControlMode>(
selector: (_, config) => config.accessControl.mode,
builder: (_, accessControlMode, __) {
return Wrap(
spacing: 16,
children: [
for (final item in AccessControlMode.values)
SettingInfoCard(
Info(
label: _getTextWithAccessControlMode(item),
iconData: _getIconWithAccessControlMode(item),
),
isSelected: accessControlMode == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
mode: item,
);
},
)
],
);
},
),
)
],
);
}
List<Widget> _buildSortSetting() {
return generateSection(
title: appLocalizations.sort,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, AccessSortType>(
selector: (_, config) => config.accessControl.sort,
builder: (_, accessSortType, __) {
return Wrap(
spacing: 16,
children: [
for (final item in AccessSortType.values)
SettingInfoCard(
Info(
label: _getTextWithAccessSortType(item),
iconData: _getIconWithProxiesSortType(item),
),
isSelected: accessSortType == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
sort: item,
);
},
),
],
);
},
),
),
],
);
}
List<Widget> _buildSourceSetting() {
return generateSection(
title: appLocalizations.source,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, bool>(
selector: (_, config) => config.accessControl.isFilterSystemApp,
builder: (_, isFilterSystemApp, __) {
return Wrap(
spacing: 16,
children: [
for (final item in [false, true])
SettingTextCard(
_getTextWithIsFilterSystemApp(item),
isSelected: isFilterSystemApp == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
isFilterSystemApp: item,
);
},
)
],
);
},
),
)
],
);
}
_intelligentSelected() async {
final appState = globalState.appController.appState;
final config = globalState.appController.config;
final accessControl = config.accessControl;
final packageNames = appState.packages
.where(
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
Navigator.of(context).pop();
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
config.accessControl = accessControl.copyWith(
acceptList: acceptList,
rejectList: rejectList,
);
}
_copyToClipboard() async {
await globalState.safeRun(() {
final data = globalState.appController.config.accessControl.toJson();
Clipboard.setData(
ClipboardData(
text: json.encode(data),
),
);
});
if (!context.mounted) return;
Navigator.of(context).pop();
}
_pasteToClipboard() async {
await globalState.safeRun(() async {
final config = globalState.appController.config;
final data = await Clipboard.getData('text/plain');
final text = data?.text;
if (text == null) return;
config.accessControl = AccessControl.fromJson(
json.decode(text),
);
});
if (!context.mounted) return;
Navigator.of(context).pop();
}
List<Widget> _buildActionSetting() {
return generateSection(
title: appLocalizations.action,
items: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: Wrap(
runSpacing: 16,
spacing: 16,
children: [
CommonChip(
avatar: const Icon(Icons.auto_awesome),
label: appLocalizations.intelligentSelected,
onPressed: _intelligentSelected,
),
CommonChip(
avatar: const Icon(Icons.paste),
label: appLocalizations.clipboardImport,
onPressed: _pasteToClipboard,
),
CommonChip(
avatar: const Icon(Icons.content_copy),
label: appLocalizations.clipboardExport,
onPressed: _copyToClipboard,
)
],
),
)
],
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildModeSetting(),
..._buildSortSetting(),
..._buildSourceSetting(),
..._buildActionSetting(),
],
),
);
} }
} }

View File

@@ -76,7 +76,7 @@ class ApplicationSettingFragment extends StatelessWidget {
selector: (_, config) => config.autoRun, selector: (_, config) => config.autoRun,
builder: (_, autoRun, child) { builder: (_, autoRun, child) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon(Icons.start), leading: const Icon(Icons.not_started),
title: Text(appLocalizations.autoRun), title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc), subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
@@ -7,7 +6,6 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/models/config.dart';
import 'package:fl_clash/models/dav.dart'; import 'package:fl_clash/models/dav.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart'; import 'package:fl_clash/widgets/text.dart';
@@ -75,10 +73,11 @@ class BackupAndRecovery extends StatelessWidget {
final res = await commonScaffoldState?.loadingRun<bool>( final res = await commonScaffoldState?.loadingRun<bool>(
() async { () async {
final backupData = await globalState.appController.backupData(); final backupData = await globalState.appController.backupData();
await picker.saveFile( final value = await picker.saveFile(
other.getBackupFileName(), other.getBackupFileName(),
Uint8List.fromList(backupData), Uint8List.fromList(backupData),
); );
if(value == null) return false;
return true; return true;
}, },
title: appLocalizations.backup, title: appLocalizations.backup,

View File

@@ -27,7 +27,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
final mixedPort = int.parse(port); final mixedPort = int.parse(port);
if (mixedPort < 1024 || mixedPort > 49151) throw "Invalid port"; if (mixedPort < 1024 || mixedPort > 49151) throw "Invalid port";
globalState.appController.clashConfig.mixedPort = mixedPort; globalState.appController.clashConfig.mixedPort = mixedPort;
globalState.appController.updateClashConfigDebounce();
} catch (e) { } catch (e) {
globalState.showMessage( globalState.showMessage(
title: appLocalizations.proxyPort, title: appLocalizations.proxyPort,
@@ -62,7 +61,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
} }
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.logLevel = value; appController.clashConfig.logLevel = value;
appController.updateClashConfigDebounce();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
@@ -100,7 +98,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (String? value) { onChanged: (String? value) {
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.globalRealUa = value; appController.clashConfig.globalRealUa = value;
appController.updateClashConfigDebounce();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
@@ -125,7 +122,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
throw "Invalid url"; throw "Invalid url";
} }
globalState.appController.config.testUrl = newTestUrl; globalState.appController.config.testUrl = newTestUrl;
globalState.appController.updateClashConfigDebounce();
} catch (e) { } catch (e) {
globalState.showMessage( globalState.showMessage(
title: appLocalizations.testUrl, title: appLocalizations.testUrl,
@@ -172,7 +168,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
items: [ items: [
if (Platform.isAndroid) ...[ if (Platform.isAndroid) ...[
Selector<Config, bool>( Selector<Config, bool>(
selector: (_, config) => config.allowBypass, selector: (_, config) => config.vpnProps.allowBypass,
builder: (_, allowBypass, __) { builder: (_, allowBypass, __) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined), leading: const Icon(Icons.arrow_forward_outlined),
@@ -181,15 +177,18 @@ class _ConfigFragmentState extends State<ConfigFragment> {
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: allowBypass, value: allowBypass,
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; final config = globalState.appController.config;
appController.config.allowBypass = value; final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
allowBypass: value,
);
}, },
), ),
); );
}, },
), ),
Selector<Config, bool>( Selector<Config, bool>(
selector: (_, config) => config.systemProxy, selector: (_, config) => config.vpnProps.systemProxy,
builder: (_, systemProxy, __) { builder: (_, systemProxy, __) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet), leading: const Icon(Icons.settings_ethernet),
@@ -198,8 +197,11 @@ class _ConfigFragmentState extends State<ConfigFragment> {
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: systemProxy, value: systemProxy,
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; final config = globalState.appController.config;
appController.config.systemProxy = value; final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
systemProxy: value,
);
}, },
), ),
); );
@@ -351,7 +353,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.ipv6 = value; appController.clashConfig.ipv6 = value;
appController.updateClashConfigDebounce();
}, },
), ),
); );
@@ -369,7 +370,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>(); final clashConfig = context.read<ClashConfig>();
clashConfig.allowLan = value; clashConfig.allowLan = value;
globalState.appController.updateClashConfigDebounce();
}, },
), ),
); );
@@ -387,7 +387,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value; appController.clashConfig.unifiedDelay = value;
appController.updateClashConfigDebounce();
}, },
), ),
); );
@@ -407,7 +406,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.findProcessMode = appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off; value ? FindProcessMode.always : FindProcessMode.off;
appController.updateClashConfigDebounce();
}, },
), ),
); );
@@ -425,7 +423,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value; appController.clashConfig.tcpConcurrent = value;
appController.updateClashConfigDebounce();
}, },
), ),
); );
@@ -446,7 +443,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
appController.clashConfig.geodataLoader = value appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative ? geodataLoaderMemconservative
: geodataLoaderStandard; : geodataLoaderStandard;
appController.updateClashConfigDebounce();
}, },
), ),
); );
@@ -466,7 +462,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.externalController = appController.clashConfig.externalController =
value ? defaultExternalController : ''; value ? defaultExternalController : '';
appController.updateClashConfigDebounce();
}, },
), ),
); );
@@ -493,7 +488,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>(); final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value); clashConfig.tun = Tun(enable: value);
globalState.appController.updateClashConfigDebounce();
}, },
), ),
); );
@@ -652,8 +646,9 @@ class _KeepAliveIntervalFormDialogState
@override @override
void initState() { void initState() {
super.initState(); super.initState();
keepAliveIntervalController = keepAliveIntervalController = TextEditingController(
TextEditingController(text: "${widget.keepAliveInterval}"); text: "${widget.keepAliveInterval}",
);
} }
_handleUpdate() async { _handleUpdate() async {

View File

@@ -1,6 +1,9 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart'; import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
import 'package:fl_clash/fragments/dashboard/status_switch.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
@@ -28,34 +31,51 @@ class _DashboardFragmentState extends State<DashboardFragment> {
child: Align( child: Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: Selector<AppState, double>( child: Selector<AppState, double>(
selector: (_, appState) => appState.viewWidth, selector: (_, appState) => appState.viewWidth,
builder: (_, viewWidth, ___) { builder: (_, viewWidth, ___) {
// final viewMode = other.getViewMode(viewWidth); final columns = max(4 * ((viewWidth / 350).ceil()), 8);
// final isDesktop = viewMode == ViewMode.desktop; final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4;
return Grid( return Grid(
crossAxisCount: max(4 * ((viewWidth / 350).ceil()), 8), crossAxisCount: columns,
crossAxisSpacing: 16, crossAxisSpacing: 16,
mainAxisSpacing: 16, mainAxisSpacing: 16,
children: const [ children: [
GridItem( const GridItem(
crossAxisCellCount: 8, crossAxisCellCount: 8,
child: NetworkSpeed(), child: NetworkSpeed(),
), ),
GridItem( if (Platform.isAndroid)
GridItem(
crossAxisCellCount: switchCount,
child: const VPNSwitch(),
),
if (system.isDesktop) ...[
GridItem(
crossAxisCellCount: switchCount,
child: const TUNSwitch(),
),
GridItem(
crossAxisCellCount: switchCount,
child: const ProxySwitch(),
),
],
const GridItem(
crossAxisCellCount: 4, crossAxisCellCount: 4,
child: OutboundMode(), child: OutboundMode(),
), ),
GridItem( const GridItem(
crossAxisCellCount: 4, crossAxisCellCount: 4,
child: NetworkDetection(), child: NetworkDetection(),
), ),
GridItem( const GridItem(
crossAxisCellCount: 4, crossAxisCellCount: 4,
child: TrafficUsage(), child: TrafficUsage(),
), ),
GridItem( const GridItem(
crossAxisCellCount: 4, crossAxisCellCount: 4,
child: IntranetIP(), child: IntranetIP(),
), ),

View File

@@ -1,4 +1,3 @@
import 'package:country_flags/country_flags.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
@@ -15,28 +14,41 @@ class NetworkDetection extends StatefulWidget {
} }
class _NetworkDetectionState extends State<NetworkDetection> { class _NetworkDetectionState extends State<NetworkDetection> {
final ipInfoNotifier = ValueNotifier<IpInfo?>(null); final networkDetectionState = ValueNotifier<NetworkDetectionState>(
final timeoutNotifier = ValueNotifier<bool>(false); const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
bool? _preIsStart; bool? _preIsStart;
Function? _checkIpDebounce; Function? _checkIpDebounce;
CancelToken? cancelToken;
_checkIp() async { _checkIp() async {
final appState = globalState.appController.appState; final appState = globalState.appController.appState;
final isInit = appState.isInit; final isInit = appState.isInit;
final isStart = appState.isStart;
if (!isInit) return; if (!isInit) return;
timeoutNotifier.value = false; final isStart = appState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return; if (_preIsStart == false && _preIsStart == isStart) return;
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
_preIsStart = isStart; _preIsStart = isStart;
ipInfoNotifier.value = null; if (cancelToken != null) {
final ipInfo = await request.checkIp(); cancelToken!.cancel();
if (ipInfo == null) { cancelToken = null;
timeoutNotifier.value = true; }
return; cancelToken = CancelToken();
} else { try {
timeoutNotifier.value = false; final ipInfo = await request.checkIp(cancelToken: cancelToken);
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: ipInfo,
);
} catch (_) {
} }
ipInfoNotifier.value = ipInfo;
} }
_checkIpContainer(Widget child) { _checkIpContainer(Widget child) {
@@ -57,17 +69,28 @@ class _NetworkDetectionState extends State<NetworkDetection> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
ipInfoNotifier.dispose(); networkDetectionState.dispose();
timeoutNotifier.dispose(); }
String countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase();
if (code.length != 2) {
return countryCode;
}
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_checkIpDebounce = debounce(_checkIp); _checkIpDebounce ??= debounce(_checkIp);
return _checkIpContainer( return _checkIpContainer(
ValueListenableBuilder<IpInfo?>( ValueListenableBuilder<NetworkDetectionState>(
valueListenable: ipInfoNotifier, valueListenable: networkDetectionState,
builder: (_, ipInfo, __) { builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isTesting = state.isTesting;
return CommonCard( return CommonCard(
onPressed: () {}, onPressed: () {},
child: Column( child: Column(
@@ -88,37 +111,38 @@ class _NetworkDetectionState extends State<NetworkDetection> {
Flexible( Flexible(
flex: 1, flex: 1,
child: FadeBox( child: FadeBox(
child: ipInfo != null child: isTesting
? CountryFlag.fromCountryCode( ? Text(
ipInfo.countryCode, appLocalizations.checking,
width: 24, maxLines: 1,
height: 24, overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.titleMedium,
) )
: ValueListenableBuilder( : ipInfo != null
valueListenable: timeoutNotifier, ? Container(
builder: (_, timeout, __) { alignment: Alignment.centerLeft,
if (timeout) { height: globalState.appController
return Text( .measure.titleMediumHeight,
appLocalizations.checkError, child: Text(
countryCodeToEmoji(
ipInfo.countryCode),
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium, .titleLarge
maxLines: 1, ?.copyWith(
overflow: TextOverflow.ellipsis, fontFamily: "Twemoji",
); ),
}
return TooltipText(
text: Text(
appLocalizations.checking,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium,
), ),
); )
}, : Text(
), appLocalizations.checkError,
style: Theme.of(context)
.textTheme
.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
), ),
], ],
@@ -151,28 +175,24 @@ class _NetworkDetectionState extends State<NetworkDetection> {
), ),
], ],
) )
: ValueListenableBuilder( : FadeBox(
valueListenable: timeoutNotifier, child: isTesting == false && ipInfo == null
builder: (_, timeout, __) { ? Text(
if (timeout) { "timeout",
return Text( style: context.textTheme.titleLarge
"timeout", ?.copyWith(color: Colors.red)
style: context.textTheme.titleLarge .toSoftBold
?.copyWith(color: Colors.red) .toMinus,
.toSoftBold maxLines: 1,
.toMinus, overflow: TextOverflow.ellipsis,
maxLines: 1, )
overflow: TextOverflow.ellipsis, : Container(
); padding: const EdgeInsets.all(2),
} child: const AspectRatio(
return Container( aspectRatio: 1,
padding: const EdgeInsets.all(2), child: CircularProgressIndicator(),
child: const AspectRatio( ),
aspectRatio: 1, ),
child: CircularProgressIndicator(),
),
);
},
), ),
), ),
) )

View File

@@ -114,7 +114,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
onPressed: () {}, onPressed: () {},
info: Info( info: Info(
label: appLocalizations.networkSpeed, label: appLocalizations.networkSpeed,
iconData: Icons.speed, iconData: Icons.speed_sharp,
), ),
child: Selector<AppState, List<Traffic>>( child: Selector<AppState, List<Traffic>>(
selector: (_, appState) => appState.traffics, selector: (_, appState) => appState.traffics,

View File

@@ -15,7 +15,6 @@ class OutboundMode extends StatelessWidget {
final clashConfig = appController.clashConfig; final clashConfig = appController.clashConfig;
if (value == null || clashConfig.mode == value) return; if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value; clashConfig.mode = value;
await appController.updateClashConfig();
appController.addCheckIpNumDebounce(); appController.addCheckIpNumDebounce();
} }
@@ -28,7 +27,7 @@ class OutboundMode extends StatelessWidget {
onPressed: () {}, onPressed: () {},
info: Info( info: Info(
label: appLocalizations.outboundMode, label: appLocalizations.outboundMode,
iconData: Icons.call_split, iconData: Icons.call_split_sharp,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),

View File

@@ -37,7 +37,7 @@ class _StartButtonState extends State<StartButton>
if (isStart == appController.appState.isStart) { if (isStart == appController.appState.isStart) {
isStart = !isStart; isStart = !isStart;
updateController(); updateController();
appController.updateSystemProxy(isStart); appController.updateStatus(isStart);
} }
} }
@@ -53,7 +53,7 @@ class _StartButtonState extends State<StartButton>
return Selector<AppState, bool>( return Selector<AppState, bool>(
selector: (_, appState) => appState.isStart, selector: (_, appState) => appState.isStart,
builder: (_, isStart, child) { builder: (_, isStart, child) {
if(isStart != this.isStart){ if (isStart != this.isStart) {
this.isStart = isStart; this.isStart = isStart;
updateController(); updateController();
} }

View File

@@ -0,0 +1,121 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class VPNSwitch extends StatelessWidget {
const VPNSwitch({super.key});
@override
Widget build(BuildContext context) {
return SwitchContainer(
info: const Info(
label: "VPN",
iconData: Icons.stacked_line_chart,
),
child: Selector<Config, bool>(
selector: (_, config) => config.vpnProps.enable,
builder: (_, enable, __) {
return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: enable,
onChanged: (value) {
final config = globalState.appController.config;
config.vpnProps = config.vpnProps.copyWith(
enable: value,
);
},
);
},
),
);
}
}
class TUNSwitch extends StatelessWidget {
const TUNSwitch({super.key});
@override
Widget build(BuildContext context) {
return SwitchContainer(
info: Info(
label: appLocalizations.tun,
iconData: Icons.stacked_line_chart,
),
child: Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, enable, __) {
return Switch(
value: enable,
onChanged: (value) {
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
);
},
);
},
),
);
}
}
class ProxySwitch extends StatelessWidget {
const ProxySwitch({super.key});
@override
Widget build(BuildContext context) {
return SwitchContainer(
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Selector<Config, bool>(
selector: (_, config) => config.desktopProps.systemProxy,
builder: (_, systemProxy, __) {
return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
final config = globalState.appController.config;
config.desktopProps =
config.desktopProps.copyWith(systemProxy: value);
},
);
},
),
);
}
}
class SwitchContainer extends StatelessWidget {
final Info info;
final Widget child;
const SwitchContainer({
super.key,
required this.info,
required this.child,
});
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader(
info: info,
actions: [
child,
],
),
],
),
);
}
}

View File

@@ -11,8 +11,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'view_profile.dart';
class EditProfile extends StatefulWidget { class EditProfile extends StatefulWidget {
final Profile profile; final Profile profile;
final BuildContext context; final BuildContext context;

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
@@ -37,21 +39,11 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
); );
} }
_getColumns(ViewMode viewMode) {
switch (viewMode) {
case ViewMode.mobile:
return 1;
case ViewMode.laptop:
return 1;
case ViewMode.desktop:
return 2;
}
}
_updateProfiles() async { _updateProfiles() async {
final appController = globalState.appController; final appController = globalState.appController;
final config = appController.config; final config = appController.config;
final profiles = appController.config.profiles; final profiles = appController.config.profiles;
final messages = [];
final updateProfiles = profiles.map<Future>( final updateProfiles = profiles.map<Future>(
(profile) async { (profile) async {
config.setProfile( config.setProfile(
@@ -60,9 +52,10 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
try { try {
await appController.updateProfile(profile); await appController.updateProfile(profile);
if (profile.id == appController.config.currentProfile?.id) { if (profile.id == appController.config.currentProfile?.id) {
appController.applyProfile(isPrue: true); appController.applyProfileDebounce();
} }
} catch (_) { } catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile( config.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: false, isUpdating: false,
@@ -71,15 +64,27 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
}, },
); );
final titleMedium = context.textTheme.titleMedium;
await Future.wait(updateProfiles); await Future.wait(updateProfiles);
if (messages.isNotEmpty) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
children: [
for (final message in messages)
TextSpan(text: message, style: titleMedium)
],
),
);
}
} }
_initScaffoldState() { _initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) { (_) {
if (!mounted) return;
final commonScaffoldState = final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>(); context.findAncestorStateOfType<CommonScaffoldState>();
if (!context.mounted) return;
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () {
@@ -87,6 +92,24 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}, },
icon: const Icon(Icons.sync), icon: const Icon(Icons.sync),
), ),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
final profiles = globalState.appController.config.profiles;
showSheet(
title: appLocalizations.profilesSort,
context: context,
builder: (_) => SizedBox(
height: 400,
child: ReorderableProfiles(profiles: profiles),
),
);
},
icon: const Icon(Icons.sort),
iconSize: 26,
),
]; ];
}, },
); );
@@ -116,7 +139,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
selector: (_, appState, config) => ProfilesSelectorState( selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles, profiles: config.profiles,
currentProfileId: config.currentProfileId, currentProfileId: config.currentProfileId,
viewMode: appState.viewMode, columns: other.getProfilesColumns(appState.viewWidth),
), ),
builder: (context, state, child) { builder: (context, state, child) {
if (state.profiles.isEmpty) { if (state.profiles.isEmpty) {
@@ -124,7 +147,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
label: appLocalizations.nullProfileDesc, label: appLocalizations.nullProfileDesc,
); );
} }
final columns = _getColumns(state.viewMode);
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -137,7 +159,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
child: Grid( child: Grid(
mainAxisSpacing: 16, mainAxisSpacing: 16,
crossAxisSpacing: 16, crossAxisSpacing: 16,
crossAxisCount: columns, crossAxisCount: state.columns,
children: [ children: [
for (int i = 0; i < state.profiles.length; i++) for (int i = 0; i < state.profiles.length; i++)
GridItem( GridItem(
@@ -145,8 +167,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
key: Key(state.profiles[i].id), key: Key(state.profiles[i].id),
profile: state.profiles[i], profile: state.profiles[i],
groupValue: state.currentProfileId, groupValue: state.currentProfileId,
onChanged: onChanged: globalState.appController.changeProfile,
globalState.appController.changeProfile,
), ),
), ),
], ],
@@ -204,7 +225,7 @@ class ProfileItem extends StatelessWidget {
); );
await appController.updateProfile(profile); await appController.updateProfile(profile);
if (profile.id == appController.config.currentProfile?.id) { if (profile.id == appController.config.currentProfile?.id) {
appController.applyProfile(isPrue: true); appController.applyProfileDebounce();
} }
} catch (e) { } catch (e) {
config.setProfile( config.setProfile(
@@ -244,6 +265,7 @@ class ProfileItem extends StatelessWidget {
LinearProgressIndicator( LinearProgressIndicator(
minHeight: 6, minHeight: 6,
value: progress, value: progress,
backgroundColor: context.colorScheme.primary.toSoft(),
), ),
const SizedBox( const SizedBox(
height: 8, height: 8,
@@ -372,3 +394,129 @@ class ProfileItem extends StatelessWidget {
); );
} }
} }
class ReorderableProfiles extends StatefulWidget {
final List<Profile> profiles;
const ReorderableProfiles({
super.key,
required this.profiles,
});
@override
State<ReorderableProfiles> createState() => _ReorderableProfilesState();
}
class _ReorderableProfilesState extends State<ReorderableProfiles> {
late List<Profile> profiles;
@override
void initState() {
super.initState();
profiles = List.from(widget.profiles);
}
Widget proxyDecorator(
Widget child,
int index,
Animation<double> animation,
) {
final profile = profiles[index];
return AnimatedBuilder(
animation: animation,
builder: (_, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
child: child,
);
},
child: Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 44,
left: 16,
),
title: Text(profile.label ?? profile.id),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 1,
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
padding: const EdgeInsets.all(12),
proxyDecorator: proxyDecorator,
onReorder: (int oldIndex, int newIndex) {
if (oldIndex == newIndex) return;
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final profile = profiles.removeAt(oldIndex);
profiles.insert(newIndex, profile);
});
},
itemBuilder: (_, index) {
final profile = profiles[index];
return Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 16,
left: 16,
),
title: Text(profile.label ?? profile.id),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
),
),
);
},
itemCount: profiles.length,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.config.profiles = profiles;
},
icon: const Icon(
Icons.check,
),
iconSize: 32,
padding: const EdgeInsets.all(8),
),
],
),
),
],
);
}
}

View File

@@ -69,23 +69,21 @@ class ProxyCard extends StatelessWidget {
if (type == ProxyCardType.min) { if (type == ProxyCardType.min) {
return SizedBox( return SizedBox(
height: measure.bodyMediumHeight * 1, height: measure.bodyMediumHeight * 1,
child: Text( child: EmojiText(
proxy.name, proxy.name,
maxLines: 1, maxLines: 1,
style: context.textTheme.bodyMedium?.copyWith( overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium,
),
), ),
); );
} else { } else {
return SizedBox( return SizedBox(
height: measure.bodyMediumHeight * 2, height: measure.bodyMediumHeight * 2,
child: Text( child: EmojiText(
proxy.name, proxy.name,
maxLines: 2, maxLines: 2,
style: context.textTheme.bodyMedium?.copyWith( overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium,
),
), ),
); );
} }
@@ -155,14 +153,12 @@ class ProxyCard extends StatelessWidget {
proxy.name, proxy.name,
), ),
builder: (_, desc, __) { builder: (_, desc, __) {
return TooltipText( return EmojiText(
text: Text( desc,
desc, overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis, color: context.textTheme.bodySmall?.color
color: context.textTheme.bodySmall?.color ?.toLight(),
?.toLight(),
),
), ),
); );
}, },

View File

@@ -1,7 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/common/other.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -61,7 +61,10 @@ double getScrollToSelectedOffset({
required List<Proxy> proxies, required List<Proxy> proxies,
}) { }) {
final appController = globalState.appController; final appController = globalState.appController;
final columns = appController.columns; final columns = other.getProxiesColumns(
appController.appState.viewWidth,
appController.config.proxiesLayout,
);
final proxyCardType = appController.config.proxyCardType; final proxyCardType = appController.config.proxyCardType;
final selectedName = appController.getCurrentSelectedName(groupName); final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere( final findSelectedIndex = proxies.indexWhere(

View File

@@ -4,6 +4,7 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart'; import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -237,7 +238,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
currentUnfoldSet: config.currentUnfoldSet, currentUnfoldSet: config.currentUnfoldSet,
proxyCardType: config.proxyCardType, proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType, proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns, columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
sortNum: appState.sortNum, sortNum: appState.sortNum,
); );
}, },
@@ -450,7 +454,7 @@ class _ListHeaderState extends State<ListHeader>
if (currentGroupName.isNotEmpty) ...[ if (currentGroupName.isNotEmpty) ...[
Flexible( Flexible(
flex: 1, flex: 1,
child: Text( child: EmojiText(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
" · $currentGroupName", " · $currentGroupName",
style: context style: context

View File

@@ -1,3 +1,6 @@
import 'dart:convert';
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/app.dart'; import 'package:fl_clash/models/app.dart';
@@ -50,7 +53,6 @@ class _ProvidersState extends State<Providers> {
); );
await clashCore.updateExternalProvider( await clashCore.updateExternalProvider(
providerName: provider.name, providerName: provider.name,
providerType: provider.type,
); );
appState.setProvider( appState.setProvider(
clashCore.getExternalProvider(provider.name), clashCore.getExternalProvider(provider.name),
@@ -58,6 +60,7 @@ class _ProvidersState extends State<Providers> {
}, },
); );
await Future.wait(updateProviders); await Future.wait(updateProviders);
await globalState.appController.updateGroupDebounce();
} }
@override @override
@@ -91,28 +94,48 @@ class ProviderItem extends StatelessWidget {
required this.provider, required this.provider,
}); });
_handleUpdateProfile() async { _handleUpdateProvider() async {
await globalState.safeRun<void>(updateProvider); await globalState.safeRun<void>(() async {
final appState = globalState.appController.appState;
if (provider.vehicleType != "HTTP") return;
await globalState.safeRun(() async {
appState.setProvider(
provider.copyWith(
isUpdating: true,
),
);
final message = await clashCore.updateExternalProvider(
providerName: provider.name,
);
if (message.isNotEmpty) throw message;
});
appState.setProvider(
clashCore.getExternalProvider(provider.name),
);
});
await globalState.appController.updateGroupDebounce();
} }
updateProvider() async { _handleSideLoadProvider() async {
final appState = globalState.appController.appState; await globalState.safeRun<void>(() async {
if (provider.vehicleType != "HTTP") return; final platformFile = await picker.pickerFile();
await globalState.safeRun(() async { final appState = globalState.appController.appState;
appState.setProvider( final bytes = platformFile?.bytes;
provider.copyWith( if (bytes == null) return;
isUpdating: true, final file = await File(provider.path).create(recursive: true);
), await file.writeAsBytes(bytes);
final providerName = provider.name;
var message = await clashCore.sideLoadExternalProvider(
providerName: providerName,
data: utf8.decode(bytes),
); );
final message = await clashCore.updateExternalProvider( if (message.isNotEmpty) throw message;
providerName: provider.name, appState.setProvider(
providerType: provider.type, clashCore.getExternalProvider(provider.name),
); );
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;
}); });
appState.setProvider( await globalState.appController.updateGroupDebounce();
clashCore.getExternalProvider(provider.name),
);
} }
String _buildProviderDesc() { String _buildProviderDesc() {
@@ -153,18 +176,16 @@ class ProviderItem extends StatelessWidget {
runSpacing: 6, runSpacing: 6,
spacing: 12, spacing: 12,
children: [ children: [
// CommonChip( CommonChip(
// avatar: const Icon(Icons.upload), avatar: const Icon(Icons.upload),
// label: appLocalizations.upload, label: appLocalizations.upload,
// onPressed: () {}, onPressed: _handleSideLoadProvider,
// ), ),
if (provider.vehicleType == "HTTP") if (provider.vehicleType == "HTTP")
CommonChip( CommonChip(
avatar: const Icon(Icons.sync), avatar: const Icon(Icons.sync),
label: appLocalizations.sync, label: appLocalizations.sync,
onPressed: () { onPressed: _handleUpdateProvider,
_handleUpdateProfile();
},
), ),
], ],
), ),

View File

@@ -33,6 +33,14 @@ class ProxiesSettingWidget extends StatelessWidget {
}; };
} }
String getTextForProxiesLayout(ProxiesLayout proxiesLayout) {
return switch (proxiesLayout) {
ProxiesLayout.tight => appLocalizations.tight,
ProxiesLayout.standard => appLocalizations.standard,
ProxiesLayout.loose => appLocalizations.loose,
};
}
List<Widget> _buildStyleSetting() { List<Widget> _buildStyleSetting() {
return generateSection( return generateSection(
title: appLocalizations.style, title: appLocalizations.style,
@@ -132,36 +140,28 @@ class ProxiesSettingWidget extends StatelessWidget {
); );
} }
List<Widget> _buildColumnsSetting() { List<Widget> _buildLayoutSetting() {
return generateSection( return generateSection(
title: appLocalizations.columns, title: appLocalizations.layout,
items: [ items: [
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
), ),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector2<AppState, Config, ColumnsSelectorState>( child: Selector< Config, ProxiesLayout>(
selector: (_, appState, config) => ColumnsSelectorState( selector: (_, config) => config.proxiesLayout,
columns: config.proxiesColumns, builder: (_, proxiesLayout, __) {
viewMode: appState.viewMode,
),
builder: (_, state, __) {
final config = globalState.appController.config; final config = globalState.appController.config;
final targetColumnsArray = viewModeColumnsMap[state.viewMode]!;
final currentColumns = other.getColumns(
state.viewMode,
state.columns,
);
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
for (final item in targetColumnsArray) for (final item in ProxiesLayout.values)
SettingTextCard( SettingTextCard(
other.getColumnsTextForInt(item), getTextForProxiesLayout(item),
isSelected: item == currentColumns, isSelected: item == proxiesLayout,
onPressed: () { onPressed: () {
config.proxiesColumns = item; config.proxiesLayout = item;
}, },
) )
], ],
@@ -183,80 +183,10 @@ class ProxiesSettingWidget extends StatelessWidget {
children: [ children: [
..._buildStyleSetting(), ..._buildStyleSetting(),
..._buildSortSetting(), ..._buildSortSetting(),
..._buildColumnsSetting(), ..._buildLayoutSetting(),
..._buildSizeSetting(), ..._buildSizeSetting(),
], ],
), ),
); );
} }
} }
class SettingInfoCard extends StatelessWidget {
final Info info;
final bool? isSelected;
final VoidCallback onPressed;
const SettingInfoCard(
this.info, {
super.key,
this.isSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
isSelected: isSelected,
onPressed: onPressed,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Icon(info.iconData),
),
const SizedBox(
width: 8,
),
Flexible(
child: Text(
info.label,
style: context.textTheme.bodyMedium,
),
),
],
),
),
);
}
}
class SettingTextCard extends StatelessWidget {
final String text;
final bool? isSelected;
final VoidCallback onPressed;
const SettingTextCard(
this.text, {
super.key,
this.isSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: onPressed,
isSelected: isSelected,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text,
style: context.textTheme.bodyMedium,
),
),
);
}
}

View File

@@ -1,7 +1,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.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/fragments/proxies/setting.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:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
@@ -285,7 +284,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return ProxyGroupSelectorState( return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType, proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType, proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns, columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
sortNum: appState.sortNum, sortNum: appState.sortNum,
proxies: group.all, proxies: group.all,
groupType: group.type, groupType: group.type,

View File

@@ -91,7 +91,6 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.geoXUrl = appController.clashConfig.geoXUrl =
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl; Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
appController.updateClashConfigDebounce();
} catch (e) { } catch (e) {
globalState.showMessage( globalState.showMessage(
title: geoItem.label, title: geoItem.label,
@@ -182,9 +181,9 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
updateGeoDateItem() async { updateGeoDateItem() async {
isUpdating.value = true; isUpdating.value = true;
try { try {
final message = await clashCore.updateExternalProvider( final message = await clashCore.updateGeoData(
providerName: geoItem.fileName, geoName: geoItem.fileName,
providerType: geoItem.label, geoType: geoItem.label,
); );
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;
} catch (e) { } catch (e) {

View File

@@ -37,7 +37,7 @@
"overrideDesc": "Override Proxy related config", "overrideDesc": "Override Proxy related config",
"allowLan": "AllowLan", "allowLan": "AllowLan",
"allowLanDesc": "Allow access proxy through the LAN", "allowLanDesc": "Allow access proxy through the LAN",
"tun": "TUN mode", "tun": "TUN",
"tunDesc": "only effective in administrator mode", "tunDesc": "only effective in administrator mode",
"minimizeOnExit": "Minimize on exit", "minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Modify the default system exit event", "minimizeOnExitDesc": "Modify the default system exit event",
@@ -117,7 +117,7 @@
"logLevel": "LogLevel", "logLevel": "LogLevel",
"show": "Show", "show": "Show",
"exit": "Exit", "exit": "Exit",
"systemProxy": "SystemProxy", "systemProxy": "System proxy",
"project": "Project", "project": "Project",
"core": "Core", "core": "Core",
"tabAnimation": "Tab animation", "tabAnimation": "Tab animation",
@@ -227,5 +227,21 @@
"remoteBackupDesc": "Backup local data to WebDAV", "remoteBackupDesc": "Backup local data to WebDAV",
"remoteRecoveryDesc": "Recovery data from WebDAV", "remoteRecoveryDesc": "Recovery data from WebDAV",
"localBackupDesc": "Backup local data to local", "localBackupDesc": "Backup local data to local",
"localRecoveryDesc": "Recovery data from file" "localRecoveryDesc": "Recovery data from file",
"mode": "Mode",
"time": "Time",
"source": "Source",
"allApps": "All apps",
"onlyOtherApps": "Only third-party apps",
"action": "Action",
"intelligentSelected": "Intelligent selection",
"clipboardImport": "Clipboard import",
"clipboardExport": "Export clipboard",
"layout": "Layout",
"tight": "Tight",
"standard": "Standard",
"loose": "Loose",
"profilesSort": "Profiles sort",
"start": "Start",
"stop": "Stop"
} }

View File

@@ -37,7 +37,7 @@
"overrideDesc": "覆写代理相关配置", "overrideDesc": "覆写代理相关配置",
"allowLan": "局域网代理", "allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理", "allowLanDesc": "允许通过局域网访问代理",
"tun": "TUN模式", "tun": "虚拟网卡",
"tunDesc": "仅在管理员模式生效", "tunDesc": "仅在管理员模式生效",
"minimizeOnExit": "退出时最小化", "minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件", "minimizeOnExitDesc": "修改系统默认退出事件",
@@ -227,5 +227,21 @@
"remoteBackupDesc": "备份数据到WebDAV", "remoteBackupDesc": "备份数据到WebDAV",
"remoteRecoveryDesc": "通过WebDAV恢复数据", "remoteRecoveryDesc": "通过WebDAV恢复数据",
"localBackupDesc": "备份数据到本地", "localBackupDesc": "备份数据到本地",
"localRecoveryDesc": "通过文件恢复数据" "localRecoveryDesc": "通过文件恢复数据",
"mode": "模式",
"time": "时间",
"source": "来源",
"allApps": "所有应用",
"onlyOtherApps": "仅第三方应用",
"action": "操作",
"intelligentSelected": "智能选择",
"clipboardImport": "剪贴板导入",
"clipboardExport": "导出剪贴板",
"layout": "布局",
"tight": "宽松",
"standard": "标准",
"loose": "紧凑",
"profilesSort": "配置排序",
"start": "启动",
"stop": "暂停"
} }

View File

@@ -33,6 +33,7 @@ class MessageLookup extends MessageLookupByLibrary {
"account": MessageLookupByLibrary.simpleMessage("Account"), "account": MessageLookupByLibrary.simpleMessage("Account"),
"accountTip": "accountTip":
MessageLookupByLibrary.simpleMessage("Account cannot be empty"), MessageLookupByLibrary.simpleMessage("Account cannot be empty"),
"action": MessageLookupByLibrary.simpleMessage("Action"),
"add": MessageLookupByLibrary.simpleMessage("Add"), "add": MessageLookupByLibrary.simpleMessage("Add"),
"address": MessageLookupByLibrary.simpleMessage("Address"), "address": MessageLookupByLibrary.simpleMessage("Address"),
"addressHelp": "addressHelp":
@@ -40,6 +41,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addressTip": MessageLookupByLibrary.simpleMessage( "addressTip": MessageLookupByLibrary.simpleMessage(
"Please enter a valid WebDAV address"), "Please enter a valid WebDAV address"),
"ago": MessageLookupByLibrary.simpleMessage(" Ago"), "ago": MessageLookupByLibrary.simpleMessage(" Ago"),
"allApps": MessageLookupByLibrary.simpleMessage("All apps"),
"allowBypass": MessageLookupByLibrary.simpleMessage( "allowBypass": MessageLookupByLibrary.simpleMessage(
"Allow applications to bypass VPN"), "Allow applications to bypass VPN"),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage( "allowBypassDesc": MessageLookupByLibrary.simpleMessage(
@@ -89,6 +91,10 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdateError": MessageLookupByLibrary.simpleMessage( "checkUpdateError": MessageLookupByLibrary.simpleMessage(
"The current application is already the latest version"), "The current application is already the latest version"),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."), "checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"clipboardExport":
MessageLookupByLibrary.simpleMessage("Export clipboard"),
"clipboardImport":
MessageLookupByLibrary.simpleMessage("Clipboard import"),
"columns": MessageLookupByLibrary.simpleMessage("Columns"), "columns": MessageLookupByLibrary.simpleMessage("Columns"),
"compatible": "compatible":
MessageLookupByLibrary.simpleMessage("Compatibility mode"), MessageLookupByLibrary.simpleMessage("Compatibility mode"),
@@ -167,6 +173,8 @@ class MessageLookup extends MessageLookupByLibrary {
"infiniteTime": "infiniteTime":
MessageLookupByLibrary.simpleMessage("Long term effective"), MessageLookupByLibrary.simpleMessage("Long term effective"),
"init": MessageLookupByLibrary.simpleMessage("Init"), "init": MessageLookupByLibrary.simpleMessage("Init"),
"intelligentSelected":
MessageLookupByLibrary.simpleMessage("Intelligent selection"),
"intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"), "intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage( "ipv6Desc": MessageLookupByLibrary.simpleMessage(
"When turned on it will be able to receive IPv6 traffic"), "When turned on it will be able to receive IPv6 traffic"),
@@ -174,6 +182,7 @@ class MessageLookup extends MessageLookupByLibrary {
"keepAliveIntervalDesc": "keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"), MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
"language": MessageLookupByLibrary.simpleMessage("Language"), "language": MessageLookupByLibrary.simpleMessage("Language"),
"layout": MessageLookupByLibrary.simpleMessage("Layout"),
"light": MessageLookupByLibrary.simpleMessage("Light"), "light": MessageLookupByLibrary.simpleMessage("Light"),
"list": MessageLookupByLibrary.simpleMessage("List"), "list": MessageLookupByLibrary.simpleMessage("List"),
"local": MessageLookupByLibrary.simpleMessage("Local"), "local": MessageLookupByLibrary.simpleMessage("Local"),
@@ -187,12 +196,14 @@ class MessageLookup extends MessageLookupByLibrary {
"Disabling will hide the log entry"), "Disabling will hide the log entry"),
"logs": MessageLookupByLibrary.simpleMessage("Logs"), "logs": MessageLookupByLibrary.simpleMessage("Logs"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"), "logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"),
"loose": MessageLookupByLibrary.simpleMessage("Loose"),
"min": MessageLookupByLibrary.simpleMessage("Min"), "min": MessageLookupByLibrary.simpleMessage("Min"),
"minimizeOnExit": "minimizeOnExit":
MessageLookupByLibrary.simpleMessage("Minimize on exit"), MessageLookupByLibrary.simpleMessage("Minimize on exit"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
"Modify the default system exit event"), "Modify the default system exit event"),
"minutes": MessageLookupByLibrary.simpleMessage("Minutes"), "minutes": MessageLookupByLibrary.simpleMessage("Minutes"),
"mode": MessageLookupByLibrary.simpleMessage("Mode"),
"months": MessageLookupByLibrary.simpleMessage("Months"), "months": MessageLookupByLibrary.simpleMessage("Months"),
"more": MessageLookupByLibrary.simpleMessage("More"), "more": MessageLookupByLibrary.simpleMessage("More"),
"name": MessageLookupByLibrary.simpleMessage("Name"), "name": MessageLookupByLibrary.simpleMessage("Name"),
@@ -216,6 +227,8 @@ class MessageLookup extends MessageLookupByLibrary {
"No profile, Please add a profile"), "No profile, Please add a profile"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"), "oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
"onlyOtherApps":
MessageLookupByLibrary.simpleMessage("Only third-party apps"),
"onlyStatisticsProxy": "onlyStatisticsProxy":
MessageLookupByLibrary.simpleMessage("Only statistics proxy"), MessageLookupByLibrary.simpleMessage("Only statistics proxy"),
"onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage( "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
@@ -255,6 +268,7 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage( "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"Please input the profile URL"), "Please input the profile URL"),
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"), "profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
"profilesSort": MessageLookupByLibrary.simpleMessage("Profiles sort"),
"project": MessageLookupByLibrary.simpleMessage("Project"), "project": MessageLookupByLibrary.simpleMessage("Project"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"), "proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxiesSetting": "proxiesSetting":
@@ -300,12 +314,16 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Start in the background"), MessageLookupByLibrary.simpleMessage("Start in the background"),
"size": MessageLookupByLibrary.simpleMessage("Size"), "size": MessageLookupByLibrary.simpleMessage("Size"),
"sort": MessageLookupByLibrary.simpleMessage("Sort"), "sort": MessageLookupByLibrary.simpleMessage("Sort"),
"source": MessageLookupByLibrary.simpleMessage("Source"),
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
"start": MessageLookupByLibrary.simpleMessage("Start"),
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), "startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"style": MessageLookupByLibrary.simpleMessage("Style"), "style": MessageLookupByLibrary.simpleMessage("Style"),
"submit": MessageLookupByLibrary.simpleMessage("Submit"), "submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"), "sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"), "systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage( "systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"), "Attach HTTP proxy to VpnService"),
"tab": MessageLookupByLibrary.simpleMessage("Tab"), "tab": MessageLookupByLibrary.simpleMessage("Tab"),
@@ -322,10 +340,12 @@ class MessageLookup extends MessageLookupByLibrary {
"Set dark mode,adjust the color"), "Set dark mode,adjust the color"),
"themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"), "themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"),
"threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"), "threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"),
"tight": MessageLookupByLibrary.simpleMessage("Tight"),
"time": MessageLookupByLibrary.simpleMessage("Time"),
"tip": MessageLookupByLibrary.simpleMessage("tip"), "tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"), "tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN mode"), "tun": MessageLookupByLibrary.simpleMessage("TUN"),
"tunDesc": MessageLookupByLibrary.simpleMessage( "tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"), "only effective in administrator mode"),
"twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"), "twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"),

View File

@@ -31,11 +31,13 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"), MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"),
"account": MessageLookupByLibrary.simpleMessage("账号"), "account": MessageLookupByLibrary.simpleMessage("账号"),
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"), "accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
"action": MessageLookupByLibrary.simpleMessage("操作"),
"add": MessageLookupByLibrary.simpleMessage("添加"), "add": MessageLookupByLibrary.simpleMessage("添加"),
"address": MessageLookupByLibrary.simpleMessage("地址"), "address": MessageLookupByLibrary.simpleMessage("地址"),
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"ago": MessageLookupByLibrary.simpleMessage(""), "ago": MessageLookupByLibrary.simpleMessage(""),
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"), "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
"allowBypassDesc": "allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
@@ -73,6 +75,8 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."), "checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
"columns": MessageLookupByLibrary.simpleMessage("列数"), "columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc": "compatibleDesc":
@@ -136,12 +140,14 @@ class MessageLookup extends MessageLookupByLibrary {
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"), "infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"), "init": MessageLookupByLibrary.simpleMessage("初始化"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"), "just": MessageLookupByLibrary.simpleMessage("刚刚"),
"keepAliveIntervalDesc": "keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"), MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
"language": MessageLookupByLibrary.simpleMessage("语言"), "language": MessageLookupByLibrary.simpleMessage("语言"),
"layout": MessageLookupByLibrary.simpleMessage("布局"),
"light": MessageLookupByLibrary.simpleMessage("浅色"), "light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"), "list": MessageLookupByLibrary.simpleMessage("列表"),
"local": MessageLookupByLibrary.simpleMessage("本地"), "local": MessageLookupByLibrary.simpleMessage("本地"),
@@ -152,11 +158,13 @@ class MessageLookup extends MessageLookupByLibrary {
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"), "logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"loose": MessageLookupByLibrary.simpleMessage("紧凑"),
"min": MessageLookupByLibrary.simpleMessage("最小"), "min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc": "minimizeOnExitDesc":
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
"minutes": MessageLookupByLibrary.simpleMessage("分钟"), "minutes": MessageLookupByLibrary.simpleMessage("分钟"),
"mode": MessageLookupByLibrary.simpleMessage("模式"),
"months": MessageLookupByLibrary.simpleMessage(""), "months": MessageLookupByLibrary.simpleMessage(""),
"more": MessageLookupByLibrary.simpleMessage("更多"), "more": MessageLookupByLibrary.simpleMessage("更多"),
"name": MessageLookupByLibrary.simpleMessage("名称"), "name": MessageLookupByLibrary.simpleMessage("名称"),
@@ -176,6 +184,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"), "oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"), "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
"onlyStatisticsProxyDesc": "onlyStatisticsProxyDesc":
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"), MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"),
@@ -207,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc": "profileUrlNullValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入配置URL"), MessageLookupByLibrary.simpleMessage("请输入配置URL"),
"profiles": MessageLookupByLibrary.simpleMessage("配置"), "profiles": MessageLookupByLibrary.simpleMessage("配置"),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"project": MessageLookupByLibrary.simpleMessage("项目"), "project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
@@ -241,7 +251,11 @@ class MessageLookup extends MessageLookupByLibrary {
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"), "size": MessageLookupByLibrary.simpleMessage("尺寸"),
"sort": MessageLookupByLibrary.simpleMessage("排序"), "sort": MessageLookupByLibrary.simpleMessage("排序"),
"source": MessageLookupByLibrary.simpleMessage("来源"),
"standard": MessageLookupByLibrary.simpleMessage("标准"),
"start": MessageLookupByLibrary.simpleMessage("启动"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"stop": MessageLookupByLibrary.simpleMessage("暂停"),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"style": MessageLookupByLibrary.simpleMessage("风格"), "style": MessageLookupByLibrary.simpleMessage("风格"),
"submit": MessageLookupByLibrary.simpleMessage("提交"), "submit": MessageLookupByLibrary.simpleMessage("提交"),
@@ -261,10 +275,12 @@ class MessageLookup extends MessageLookupByLibrary {
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"), "threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tight": MessageLookupByLibrary.simpleMessage("宽松"),
"time": MessageLookupByLibrary.simpleMessage("时间"),
"tip": MessageLookupByLibrary.simpleMessage("提示"), "tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"), "tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("TUN模式"), "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"), "tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"), "twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"unableToUpdateCurrentProfileDesc": "unableToUpdateCurrentProfileDesc":

View File

@@ -430,10 +430,10 @@ class AppLocalizations {
); );
} }
/// `TUN mode` /// `TUN`
String get tun { String get tun {
return Intl.message( return Intl.message(
'TUN mode', 'TUN',
name: 'tun', name: 'tun',
desc: '', desc: '',
args: [], args: [],
@@ -1230,10 +1230,10 @@ class AppLocalizations {
); );
} }
/// `SystemProxy` /// `System proxy`
String get systemProxy { String get systemProxy {
return Intl.message( return Intl.message(
'SystemProxy', 'System proxy',
name: 'systemProxy', name: 'systemProxy',
desc: '', desc: '',
args: [], args: [],
@@ -2339,6 +2339,166 @@ class AppLocalizations {
args: [], args: [],
); );
} }
/// `Mode`
String get mode {
return Intl.message(
'Mode',
name: 'mode',
desc: '',
args: [],
);
}
/// `Time`
String get time {
return Intl.message(
'Time',
name: 'time',
desc: '',
args: [],
);
}
/// `Source`
String get source {
return Intl.message(
'Source',
name: 'source',
desc: '',
args: [],
);
}
/// `All apps`
String get allApps {
return Intl.message(
'All apps',
name: 'allApps',
desc: '',
args: [],
);
}
/// `Only third-party apps`
String get onlyOtherApps {
return Intl.message(
'Only third-party apps',
name: 'onlyOtherApps',
desc: '',
args: [],
);
}
/// `Action`
String get action {
return Intl.message(
'Action',
name: 'action',
desc: '',
args: [],
);
}
/// `Intelligent selection`
String get intelligentSelected {
return Intl.message(
'Intelligent selection',
name: 'intelligentSelected',
desc: '',
args: [],
);
}
/// `Clipboard import`
String get clipboardImport {
return Intl.message(
'Clipboard import',
name: 'clipboardImport',
desc: '',
args: [],
);
}
/// `Export clipboard`
String get clipboardExport {
return Intl.message(
'Export clipboard',
name: 'clipboardExport',
desc: '',
args: [],
);
}
/// `Layout`
String get layout {
return Intl.message(
'Layout',
name: 'layout',
desc: '',
args: [],
);
}
/// `Tight`
String get tight {
return Intl.message(
'Tight',
name: 'tight',
desc: '',
args: [],
);
}
/// `Standard`
String get standard {
return Intl.message(
'Standard',
name: 'standard',
desc: '',
args: [],
);
}
/// `Loose`
String get loose {
return Intl.message(
'Loose',
name: 'loose',
desc: '',
args: [],
);
}
/// `Profiles sort`
String get profilesSort {
return Intl.message(
'Profiles sort',
name: 'profilesSort',
desc: '',
args: [],
);
}
/// `Start`
String get start {
return Intl.message(
'Start',
name: 'start',
desc: '',
args: [],
);
}
/// `Stop`
String get stop {
return Intl.message(
'Stop',
name: 'stop',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -3,8 +3,8 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/plugins/tile.dart';
import 'package:fl_clash/plugins/vpn.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:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -18,6 +18,7 @@ Future<void> main() async {
clashCore.initMessage(); clashCore.initMessage();
globalState.packageInfo = await PackageInfo.fromPlatform(); globalState.packageInfo = await PackageInfo.fromPlatform();
final config = await preferences.getConfig() ?? Config(); final config = await preferences.getConfig() ?? Config();
globalState.autoRun = config.autoRun;
final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await android?.init(); await android?.init();
await window?.init(config.windowProps); await window?.init(config.windowProps);
@@ -61,14 +62,14 @@ Future<void> vpnService() async {
clashConfig: clashConfig, clashConfig: clashConfig,
); );
proxy?.setServiceMessageHandler( vpn?.setServiceMessageHandler(
ServiceMessageHandler( ServiceMessageHandler(
onProtect: (Fd fd) async { onProtect: (Fd fd) async {
await proxy?.setProtect(fd.value); await vpn?.setProtect(fd.value);
clashCore.setFdMap(fd.id); clashCore.setFdMap(fd.id);
}, },
onProcess: (Process process) async { onProcess: (Process process) async {
var packageName = await app?.resolverProcess(process); final packageName = await app?.resolverProcess(process);
clashCore.setProcessMap( clashCore.setProcessMap(
ProcessMapItem( ProcessMapItem(
id: process.id, id: process.id,
@@ -76,8 +77,8 @@ Future<void> vpnService() async {
), ),
); );
}, },
onStarted: (String runTime) { onStarted: (String runTime) async {
globalState.applyProfile( await globalState.applyProfile(
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
@@ -100,8 +101,7 @@ Future<void> vpnService() async {
WidgetsBinding.instance.platformDispatcher.locale, WidgetsBinding.instance.platformDispatcher.locale,
); );
await app?.tip(appLocalizations.startVpn); await app?.tip(appLocalizations.startVpn);
await globalState.startSystemProxy( await globalState.handleStart(
appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
@@ -110,7 +110,7 @@ Future<void> vpnService() async {
TileListenerWithVpn( TileListenerWithVpn(
onStop: () async { onStop: () async {
await app?.tip(appLocalizations.stopVpn); await app?.tip(appLocalizations.stopVpn);
await globalState.stopSystemProxy(); await globalState.handleStop();
clashCore.shutdown(); clashCore.shutdown();
exit(0); exit(0);
}, },
@@ -130,13 +130,13 @@ class ServiceMessageHandler with ServiceMessageListener {
final Function(Fd fd) _onProtect; final Function(Fd fd) _onProtect;
final Function(Process process) _onProcess; final Function(Process process) _onProcess;
final Function(String runTime) _onStarted; final Function(String runTime) _onStarted;
final Function(String groupName) _onLoaded; final Function(String providerName) _onLoaded;
const ServiceMessageHandler({ const ServiceMessageHandler({
required Function(Fd fd) onProtect, required Function(Fd fd) onProtect,
required Function(Process process) onProcess, required Function(Process process) onProcess,
required Function(String runTime) onStarted, required Function(String runTime) onStarted,
required Function(String groupName) onLoaded, required Function(String providerName) onLoaded,
}) : _onProtect = onProtect, }) : _onProtect = onProtect,
_onProcess = onProcess, _onProcess = onProcess,
_onStarted = onStarted, _onStarted = onStarted,
@@ -158,8 +158,8 @@ class ServiceMessageHandler with ServiceMessageListener {
} }
@override @override
onLoaded(String groupName) { onLoaded(String providerName) {
_onLoaded(groupName); _onLoaded(providerName);
} }
} }

View File

@@ -353,7 +353,8 @@ class AppState with ChangeNotifier {
} }
} }
setProvider(ExternalProvider provider) { setProvider(ExternalProvider? provider) {
if(provider == null) return;
final index = _providers.indexWhere((item) => item.name == provider.name); final index = _providers.indexWhere((item) => item.name == provider.name);
if (index == -1) return; if (index == -1) return;
_providers = List.from(_providers)..[index] = provider; _providers = List.from(_providers)..[index] = provider;

View File

@@ -18,6 +18,7 @@ class AccessControl with _$AccessControl {
@Default(AccessControlMode.rejectSelected) AccessControlMode mode, @Default(AccessControlMode.rejectSelected) AccessControlMode mode,
@Default([]) List<String> acceptList, @Default([]) List<String> acceptList,
@Default([]) List<String> rejectList, @Default([]) List<String> rejectList,
@Default(AccessSortType.none) AccessSortType sort,
@Default(true) bool isFilterSystemApp, @Default(true) bool isFilterSystemApp,
}) = _AccessControl; }) = _AccessControl;
@@ -25,18 +26,27 @@ class AccessControl with _$AccessControl {
_$AccessControlFromJson(json); _$AccessControlFromJson(json);
} }
extension AccessControlExt on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
@freezed @freezed
class CoreState with _$CoreState { class CoreState with _$CoreState {
const factory CoreState({ const factory CoreState({
AccessControl? accessControl, AccessControl? accessControl,
required String currentProfileName, required String currentProfileName,
required bool enable,
required bool allowBypass, required bool allowBypass,
required bool systemProxy, required bool systemProxy,
required int mixedPort, required int mixedPort,
required bool onlyProxy, required bool onlyProxy,
}) = _CoreState; }) = _CoreState;
factory CoreState.fromJson(Map<String, Object?> json) => _$CoreStateFromJson(json); factory CoreState.fromJson(Map<String, Object?> json) =>
_$CoreStateFromJson(json);
} }
@freezed @freezed
@@ -49,10 +59,30 @@ class WindowProps with _$WindowProps {
}) = _WindowProps; }) = _WindowProps;
factory WindowProps.fromJson(Map<String, Object?>? json) => factory WindowProps.fromJson(Map<String, Object?>? json) =>
json == null ? defaultWindowProps : _$WindowPropsFromJson(json); json == null ? const WindowProps() : _$WindowPropsFromJson(json);
} }
const defaultWindowProps = WindowProps(); @freezed
class VpnProps with _$VpnProps {
const factory VpnProps({
@Default(true) bool enable,
@Default(false) bool systemProxy,
@Default(true) bool allowBypass,
}) = _VpnProps;
factory VpnProps.fromJson(Map<String, Object?>? json) =>
json == null ? const VpnProps() : _$VpnPropsFromJson(json);
}
@freezed
class DesktopProps with _$DesktopProps {
const factory DesktopProps({
@Default(true) bool systemProxy,
}) = _DesktopProps;
factory DesktopProps.fromJson(Map<String, Object?>? json) =>
json == null ? const DesktopProps() : _$DesktopPropsFromJson(json);
}
@JsonSerializable() @JsonSerializable()
class Config extends ChangeNotifier { class Config extends ChangeNotifier {
@@ -72,18 +102,19 @@ class Config extends ChangeNotifier {
AccessControl _accessControl; AccessControl _accessControl;
bool _isAnimateToPage; bool _isAnimateToPage;
bool _autoCheckUpdate; bool _autoCheckUpdate;
bool _allowBypass;
bool _systemProxy;
bool _isExclude; bool _isExclude;
DAV? _dav; DAV? _dav;
bool _isCloseConnections; bool _isCloseConnections;
ProxiesType _proxiesType; ProxiesType _proxiesType;
ProxyCardType _proxyCardType; ProxyCardType _proxyCardType;
int _proxiesColumns; ProxiesLayout _proxiesLayout;
String _testUrl; String _testUrl;
WindowProps _windowProps; WindowProps _windowProps;
bool _onlyProxy; bool _onlyProxy;
bool _prueBlack; bool _prueBlack;
VpnProps _vpnProps;
DesktopProps _desktopProps;
bool _showLabel;
Config() Config()
: _profiles = [], : _profiles = [],
@@ -99,18 +130,19 @@ class Config extends ChangeNotifier {
_isMinimizeOnExit = true, _isMinimizeOnExit = true,
_isAccessControl = false, _isAccessControl = false,
_autoCheckUpdate = true, _autoCheckUpdate = true,
_systemProxy = false,
_testUrl = defaultTestUrl, _testUrl = defaultTestUrl,
_accessControl = const AccessControl(), _accessControl = const AccessControl(),
_isAnimateToPage = true, _isAnimateToPage = true,
_allowBypass = true,
_isExclude = false, _isExclude = false,
_proxyCardType = ProxyCardType.expand, _proxyCardType = ProxyCardType.expand,
_windowProps = defaultWindowProps, _windowProps = const WindowProps(),
_proxiesType = ProxiesType.tab, _proxiesType = ProxiesType.tab,
_proxiesColumns = 2,
_prueBlack = false, _prueBlack = false,
_onlyProxy = false; _onlyProxy = false,
_proxiesLayout = ProxiesLayout.standard,
_vpnProps = const VpnProps(),
_desktopProps = const DesktopProps(),
_showLabel = false;
deleteProfileById(String id) { deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList(); _profiles = profiles.where((element) => element.id != id).toList();
@@ -312,6 +344,16 @@ class Config extends ChangeNotifier {
} }
} }
@JsonKey(defaultValue: ProxiesLayout.standard)
ProxiesLayout get proxiesLayout => _proxiesLayout;
set proxiesLayout(ProxiesLayout value) {
if (_proxiesLayout != value) {
_proxiesLayout = value;
notifyListeners();
}
}
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool get isMinimizeOnExit => _isMinimizeOnExit; bool get isMinimizeOnExit => _isMinimizeOnExit;
@@ -390,30 +432,6 @@ class Config extends ChangeNotifier {
} }
} }
@JsonKey(defaultValue: true)
bool get allowBypass {
return _allowBypass;
}
set allowBypass(bool value) {
if (_allowBypass != value) {
_allowBypass = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false)
bool get systemProxy {
return _systemProxy;
}
set systemProxy(bool value) {
if (_systemProxy != value) {
_systemProxy = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool get onlyProxy { bool get onlyProxy {
return _onlyProxy; return _onlyProxy;
@@ -473,16 +491,6 @@ class Config extends ChangeNotifier {
} }
} }
@JsonKey(defaultValue: 2)
int get proxiesColumns => _proxiesColumns;
set proxiesColumns(int value) {
if (_proxiesColumns != value) {
_proxiesColumns = value;
notifyListeners();
}
}
@JsonKey(name: "test-url", defaultValue: defaultTestUrl) @JsonKey(name: "test-url", defaultValue: defaultTestUrl)
String get testUrl => _testUrl; String get testUrl => _testUrl;
@@ -512,6 +520,34 @@ class Config extends ChangeNotifier {
} }
} }
VpnProps get vpnProps => _vpnProps;
set vpnProps(VpnProps value) {
if (_vpnProps != value) {
_vpnProps = value;
notifyListeners();
}
}
DesktopProps get desktopProps => _desktopProps;
set desktopProps(DesktopProps value) {
if (_desktopProps != value) {
_desktopProps = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false)
bool get showLabel => _showLabel;
set showLabel(bool value) {
if (_showLabel != value) {
_showLabel = value;
notifyListeners();
}
}
update([ update([
Config? config, Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all, RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -536,7 +572,6 @@ class Config extends ChangeNotifier {
_openLog = config._openLog; _openLog = config._openLog;
_themeMode = config._themeMode; _themeMode = config._themeMode;
_locale = config._locale; _locale = config._locale;
_allowBypass = config._allowBypass;
_primaryColor = config._primaryColor; _primaryColor = config._primaryColor;
_proxiesSortType = config._proxiesSortType; _proxiesSortType = config._proxiesSortType;
_isMinimizeOnExit = config._isMinimizeOnExit; _isMinimizeOnExit = config._isMinimizeOnExit;
@@ -548,6 +583,8 @@ class Config extends ChangeNotifier {
_testUrl = config._testUrl; _testUrl = config._testUrl;
_isExclude = config._isExclude; _isExclude = config._isExclude;
_windowProps = config._windowProps; _windowProps = config._windowProps;
_vpnProps = config._vpnProps;
_desktopProps = config._desktopProps;
} }
notifyListeners(); notifyListeners();
} }

View File

@@ -23,6 +23,7 @@ mixin _$AccessControl {
AccessControlMode get mode => throw _privateConstructorUsedError; AccessControlMode get mode => throw _privateConstructorUsedError;
List<String> get acceptList => throw _privateConstructorUsedError; List<String> get acceptList => throw _privateConstructorUsedError;
List<String> get rejectList => throw _privateConstructorUsedError; List<String> get rejectList => throw _privateConstructorUsedError;
AccessSortType get sort => throw _privateConstructorUsedError;
bool get isFilterSystemApp => throw _privateConstructorUsedError; bool get isFilterSystemApp => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -41,6 +42,7 @@ abstract class $AccessControlCopyWith<$Res> {
{AccessControlMode mode, {AccessControlMode mode,
List<String> acceptList, List<String> acceptList,
List<String> rejectList, List<String> rejectList,
AccessSortType sort,
bool isFilterSystemApp}); bool isFilterSystemApp});
} }
@@ -60,6 +62,7 @@ class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl>
Object? mode = null, Object? mode = null,
Object? acceptList = null, Object? acceptList = null,
Object? rejectList = null, Object? rejectList = null,
Object? sort = null,
Object? isFilterSystemApp = null, Object? isFilterSystemApp = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
@@ -75,6 +78,10 @@ class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl>
? _value.rejectList ? _value.rejectList
: rejectList // ignore: cast_nullable_to_non_nullable : rejectList // ignore: cast_nullable_to_non_nullable
as List<String>, as List<String>,
sort: null == sort
? _value.sort
: sort // ignore: cast_nullable_to_non_nullable
as AccessSortType,
isFilterSystemApp: null == isFilterSystemApp isFilterSystemApp: null == isFilterSystemApp
? _value.isFilterSystemApp ? _value.isFilterSystemApp
: isFilterSystemApp // ignore: cast_nullable_to_non_nullable : isFilterSystemApp // ignore: cast_nullable_to_non_nullable
@@ -95,6 +102,7 @@ abstract class _$$AccessControlImplCopyWith<$Res>
{AccessControlMode mode, {AccessControlMode mode,
List<String> acceptList, List<String> acceptList,
List<String> rejectList, List<String> rejectList,
AccessSortType sort,
bool isFilterSystemApp}); bool isFilterSystemApp});
} }
@@ -112,6 +120,7 @@ class __$$AccessControlImplCopyWithImpl<$Res>
Object? mode = null, Object? mode = null,
Object? acceptList = null, Object? acceptList = null,
Object? rejectList = null, Object? rejectList = null,
Object? sort = null,
Object? isFilterSystemApp = null, Object? isFilterSystemApp = null,
}) { }) {
return _then(_$AccessControlImpl( return _then(_$AccessControlImpl(
@@ -127,6 +136,10 @@ class __$$AccessControlImplCopyWithImpl<$Res>
? _value._rejectList ? _value._rejectList
: rejectList // ignore: cast_nullable_to_non_nullable : rejectList // ignore: cast_nullable_to_non_nullable
as List<String>, as List<String>,
sort: null == sort
? _value.sort
: sort // ignore: cast_nullable_to_non_nullable
as AccessSortType,
isFilterSystemApp: null == isFilterSystemApp isFilterSystemApp: null == isFilterSystemApp
? _value.isFilterSystemApp ? _value.isFilterSystemApp
: isFilterSystemApp // ignore: cast_nullable_to_non_nullable : isFilterSystemApp // ignore: cast_nullable_to_non_nullable
@@ -142,6 +155,7 @@ class _$AccessControlImpl implements _AccessControl {
{this.mode = AccessControlMode.rejectSelected, {this.mode = AccessControlMode.rejectSelected,
final List<String> acceptList = const [], final List<String> acceptList = const [],
final List<String> rejectList = const [], final List<String> rejectList = const [],
this.sort = AccessSortType.none,
this.isFilterSystemApp = true}) this.isFilterSystemApp = true})
: _acceptList = acceptList, : _acceptList = acceptList,
_rejectList = rejectList; _rejectList = rejectList;
@@ -170,13 +184,16 @@ class _$AccessControlImpl implements _AccessControl {
return EqualUnmodifiableListView(_rejectList); return EqualUnmodifiableListView(_rejectList);
} }
@override
@JsonKey()
final AccessSortType sort;
@override @override
@JsonKey() @JsonKey()
final bool isFilterSystemApp; final bool isFilterSystemApp;
@override @override
String toString() { String toString() {
return 'AccessControl(mode: $mode, acceptList: $acceptList, rejectList: $rejectList, isFilterSystemApp: $isFilterSystemApp)'; return 'AccessControl(mode: $mode, acceptList: $acceptList, rejectList: $rejectList, sort: $sort, isFilterSystemApp: $isFilterSystemApp)';
} }
@override @override
@@ -189,6 +206,7 @@ class _$AccessControlImpl implements _AccessControl {
.equals(other._acceptList, _acceptList) && .equals(other._acceptList, _acceptList) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._rejectList, _rejectList) && .equals(other._rejectList, _rejectList) &&
(identical(other.sort, sort) || other.sort == sort) &&
(identical(other.isFilterSystemApp, isFilterSystemApp) || (identical(other.isFilterSystemApp, isFilterSystemApp) ||
other.isFilterSystemApp == isFilterSystemApp)); other.isFilterSystemApp == isFilterSystemApp));
} }
@@ -200,6 +218,7 @@ class _$AccessControlImpl implements _AccessControl {
mode, mode,
const DeepCollectionEquality().hash(_acceptList), const DeepCollectionEquality().hash(_acceptList),
const DeepCollectionEquality().hash(_rejectList), const DeepCollectionEquality().hash(_rejectList),
sort,
isFilterSystemApp); isFilterSystemApp);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -221,6 +240,7 @@ abstract class _AccessControl implements AccessControl {
{final AccessControlMode mode, {final AccessControlMode mode,
final List<String> acceptList, final List<String> acceptList,
final List<String> rejectList, final List<String> rejectList,
final AccessSortType sort,
final bool isFilterSystemApp}) = _$AccessControlImpl; final bool isFilterSystemApp}) = _$AccessControlImpl;
factory _AccessControl.fromJson(Map<String, dynamic> json) = factory _AccessControl.fromJson(Map<String, dynamic> json) =
@@ -233,6 +253,8 @@ abstract class _AccessControl implements AccessControl {
@override @override
List<String> get rejectList; List<String> get rejectList;
@override @override
AccessSortType get sort;
@override
bool get isFilterSystemApp; bool get isFilterSystemApp;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -248,6 +270,7 @@ CoreState _$CoreStateFromJson(Map<String, dynamic> json) {
mixin _$CoreState { mixin _$CoreState {
AccessControl? get accessControl => throw _privateConstructorUsedError; AccessControl? get accessControl => throw _privateConstructorUsedError;
String get currentProfileName => throw _privateConstructorUsedError; String get currentProfileName => throw _privateConstructorUsedError;
bool get enable => throw _privateConstructorUsedError;
bool get allowBypass => throw _privateConstructorUsedError; bool get allowBypass => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError; bool get systemProxy => throw _privateConstructorUsedError;
int get mixedPort => throw _privateConstructorUsedError; int get mixedPort => throw _privateConstructorUsedError;
@@ -267,6 +290,7 @@ abstract class $CoreStateCopyWith<$Res> {
$Res call( $Res call(
{AccessControl? accessControl, {AccessControl? accessControl,
String currentProfileName, String currentProfileName,
bool enable,
bool allowBypass, bool allowBypass,
bool systemProxy, bool systemProxy,
int mixedPort, int mixedPort,
@@ -290,6 +314,7 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
$Res call({ $Res call({
Object? accessControl = freezed, Object? accessControl = freezed,
Object? currentProfileName = null, Object? currentProfileName = null,
Object? enable = null,
Object? allowBypass = null, Object? allowBypass = null,
Object? systemProxy = null, Object? systemProxy = null,
Object? mixedPort = null, Object? mixedPort = null,
@@ -304,6 +329,10 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
? _value.currentProfileName ? _value.currentProfileName
: currentProfileName // ignore: cast_nullable_to_non_nullable : currentProfileName // ignore: cast_nullable_to_non_nullable
as String, as String,
enable: null == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as bool,
allowBypass: null == allowBypass allowBypass: null == allowBypass
? _value.allowBypass ? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable : allowBypass // ignore: cast_nullable_to_non_nullable
@@ -347,6 +376,7 @@ abstract class _$$CoreStateImplCopyWith<$Res>
$Res call( $Res call(
{AccessControl? accessControl, {AccessControl? accessControl,
String currentProfileName, String currentProfileName,
bool enable,
bool allowBypass, bool allowBypass,
bool systemProxy, bool systemProxy,
int mixedPort, int mixedPort,
@@ -369,6 +399,7 @@ class __$$CoreStateImplCopyWithImpl<$Res>
$Res call({ $Res call({
Object? accessControl = freezed, Object? accessControl = freezed,
Object? currentProfileName = null, Object? currentProfileName = null,
Object? enable = null,
Object? allowBypass = null, Object? allowBypass = null,
Object? systemProxy = null, Object? systemProxy = null,
Object? mixedPort = null, Object? mixedPort = null,
@@ -383,6 +414,10 @@ class __$$CoreStateImplCopyWithImpl<$Res>
? _value.currentProfileName ? _value.currentProfileName
: currentProfileName // ignore: cast_nullable_to_non_nullable : currentProfileName // ignore: cast_nullable_to_non_nullable
as String, as String,
enable: null == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as bool,
allowBypass: null == allowBypass allowBypass: null == allowBypass
? _value.allowBypass ? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable : allowBypass // ignore: cast_nullable_to_non_nullable
@@ -409,6 +444,7 @@ class _$CoreStateImpl implements _CoreState {
const _$CoreStateImpl( const _$CoreStateImpl(
{this.accessControl, {this.accessControl,
required this.currentProfileName, required this.currentProfileName,
required this.enable,
required this.allowBypass, required this.allowBypass,
required this.systemProxy, required this.systemProxy,
required this.mixedPort, required this.mixedPort,
@@ -422,6 +458,8 @@ class _$CoreStateImpl implements _CoreState {
@override @override
final String currentProfileName; final String currentProfileName;
@override @override
final bool enable;
@override
final bool allowBypass; final bool allowBypass;
@override @override
final bool systemProxy; final bool systemProxy;
@@ -432,7 +470,7 @@ class _$CoreStateImpl implements _CoreState {
@override @override
String toString() { String toString() {
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)'; return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
} }
@override @override
@@ -444,6 +482,7 @@ class _$CoreStateImpl implements _CoreState {
other.accessControl == accessControl) && other.accessControl == accessControl) &&
(identical(other.currentProfileName, currentProfileName) || (identical(other.currentProfileName, currentProfileName) ||
other.currentProfileName == currentProfileName) && other.currentProfileName == currentProfileName) &&
(identical(other.enable, enable) || other.enable == enable) &&
(identical(other.allowBypass, allowBypass) || (identical(other.allowBypass, allowBypass) ||
other.allowBypass == allowBypass) && other.allowBypass == allowBypass) &&
(identical(other.systemProxy, systemProxy) || (identical(other.systemProxy, systemProxy) ||
@@ -456,8 +495,15 @@ class _$CoreStateImpl implements _CoreState {
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => Object.hash(runtimeType, accessControl, int get hashCode => Object.hash(
currentProfileName, allowBypass, systemProxy, mixedPort, onlyProxy); runtimeType,
accessControl,
currentProfileName,
enable,
allowBypass,
systemProxy,
mixedPort,
onlyProxy);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@@ -477,6 +523,7 @@ abstract class _CoreState implements CoreState {
const factory _CoreState( const factory _CoreState(
{final AccessControl? accessControl, {final AccessControl? accessControl,
required final String currentProfileName, required final String currentProfileName,
required final bool enable,
required final bool allowBypass, required final bool allowBypass,
required final bool systemProxy, required final bool systemProxy,
required final int mixedPort, required final int mixedPort,
@@ -490,6 +537,8 @@ abstract class _CoreState implements CoreState {
@override @override
String get currentProfileName; String get currentProfileName;
@override @override
bool get enable;
@override
bool get allowBypass; bool get allowBypass;
@override @override
bool get systemProxy; bool get systemProxy;
@@ -693,3 +742,318 @@ abstract class _WindowProps implements WindowProps {
_$$WindowPropsImplCopyWith<_$WindowPropsImpl> get copyWith => _$$WindowPropsImplCopyWith<_$WindowPropsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
VpnProps _$VpnPropsFromJson(Map<String, dynamic> json) {
return _VpnProps.fromJson(json);
}
/// @nodoc
mixin _$VpnProps {
bool get enable => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError;
bool get allowBypass => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$VpnPropsCopyWith<VpnProps> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $VpnPropsCopyWith<$Res> {
factory $VpnPropsCopyWith(VpnProps value, $Res Function(VpnProps) then) =
_$VpnPropsCopyWithImpl<$Res, VpnProps>;
@useResult
$Res call({bool enable, bool systemProxy, bool allowBypass});
}
/// @nodoc
class _$VpnPropsCopyWithImpl<$Res, $Val extends VpnProps>
implements $VpnPropsCopyWith<$Res> {
_$VpnPropsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? enable = null,
Object? systemProxy = null,
Object? allowBypass = null,
}) {
return _then(_value.copyWith(
enable: null == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as bool,
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool,
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$VpnPropsImplCopyWith<$Res>
implements $VpnPropsCopyWith<$Res> {
factory _$$VpnPropsImplCopyWith(
_$VpnPropsImpl value, $Res Function(_$VpnPropsImpl) then) =
__$$VpnPropsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool enable, bool systemProxy, bool allowBypass});
}
/// @nodoc
class __$$VpnPropsImplCopyWithImpl<$Res>
extends _$VpnPropsCopyWithImpl<$Res, _$VpnPropsImpl>
implements _$$VpnPropsImplCopyWith<$Res> {
__$$VpnPropsImplCopyWithImpl(
_$VpnPropsImpl _value, $Res Function(_$VpnPropsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? enable = null,
Object? systemProxy = null,
Object? allowBypass = null,
}) {
return _then(_$VpnPropsImpl(
enable: null == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as bool,
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool,
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$VpnPropsImpl implements _VpnProps {
const _$VpnPropsImpl(
{this.enable = true, this.systemProxy = false, this.allowBypass = true});
factory _$VpnPropsImpl.fromJson(Map<String, dynamic> json) =>
_$$VpnPropsImplFromJson(json);
@override
@JsonKey()
final bool enable;
@override
@JsonKey()
final bool systemProxy;
@override
@JsonKey()
final bool allowBypass;
@override
String toString() {
return 'VpnProps(enable: $enable, systemProxy: $systemProxy, allowBypass: $allowBypass)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$VpnPropsImpl &&
(identical(other.enable, enable) || other.enable == enable) &&
(identical(other.systemProxy, systemProxy) ||
other.systemProxy == systemProxy) &&
(identical(other.allowBypass, allowBypass) ||
other.allowBypass == allowBypass));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, enable, systemProxy, allowBypass);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$VpnPropsImplCopyWith<_$VpnPropsImpl> get copyWith =>
__$$VpnPropsImplCopyWithImpl<_$VpnPropsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$VpnPropsImplToJson(
this,
);
}
}
abstract class _VpnProps implements VpnProps {
const factory _VpnProps(
{final bool enable,
final bool systemProxy,
final bool allowBypass}) = _$VpnPropsImpl;
factory _VpnProps.fromJson(Map<String, dynamic> json) =
_$VpnPropsImpl.fromJson;
@override
bool get enable;
@override
bool get systemProxy;
@override
bool get allowBypass;
@override
@JsonKey(ignore: true)
_$$VpnPropsImplCopyWith<_$VpnPropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}
DesktopProps _$DesktopPropsFromJson(Map<String, dynamic> json) {
return _DesktopProps.fromJson(json);
}
/// @nodoc
mixin _$DesktopProps {
bool get systemProxy => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DesktopPropsCopyWith<DesktopProps> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DesktopPropsCopyWith<$Res> {
factory $DesktopPropsCopyWith(
DesktopProps value, $Res Function(DesktopProps) then) =
_$DesktopPropsCopyWithImpl<$Res, DesktopProps>;
@useResult
$Res call({bool systemProxy});
}
/// @nodoc
class _$DesktopPropsCopyWithImpl<$Res, $Val extends DesktopProps>
implements $DesktopPropsCopyWith<$Res> {
_$DesktopPropsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? systemProxy = null,
}) {
return _then(_value.copyWith(
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$DesktopPropsImplCopyWith<$Res>
implements $DesktopPropsCopyWith<$Res> {
factory _$$DesktopPropsImplCopyWith(
_$DesktopPropsImpl value, $Res Function(_$DesktopPropsImpl) then) =
__$$DesktopPropsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool systemProxy});
}
/// @nodoc
class __$$DesktopPropsImplCopyWithImpl<$Res>
extends _$DesktopPropsCopyWithImpl<$Res, _$DesktopPropsImpl>
implements _$$DesktopPropsImplCopyWith<$Res> {
__$$DesktopPropsImplCopyWithImpl(
_$DesktopPropsImpl _value, $Res Function(_$DesktopPropsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? systemProxy = null,
}) {
return _then(_$DesktopPropsImpl(
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$DesktopPropsImpl implements _DesktopProps {
const _$DesktopPropsImpl({this.systemProxy = true});
factory _$DesktopPropsImpl.fromJson(Map<String, dynamic> json) =>
_$$DesktopPropsImplFromJson(json);
@override
@JsonKey()
final bool systemProxy;
@override
String toString() {
return 'DesktopProps(systemProxy: $systemProxy)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$DesktopPropsImpl &&
(identical(other.systemProxy, systemProxy) ||
other.systemProxy == systemProxy));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, systemProxy);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$DesktopPropsImplCopyWith<_$DesktopPropsImpl> get copyWith =>
__$$DesktopPropsImplCopyWithImpl<_$DesktopPropsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$DesktopPropsImplToJson(
this,
);
}
}
abstract class _DesktopProps implements DesktopProps {
const factory _DesktopProps({final bool systemProxy}) = _$DesktopPropsImpl;
factory _DesktopProps.fromJson(Map<String, dynamic> json) =
_$DesktopPropsImpl.fromJson;
@override
bool get systemProxy;
@override
@JsonKey(ignore: true)
_$$DesktopPropsImplCopyWith<_$DesktopPropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -23,6 +23,9 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..proxiesSortType = ..proxiesSortType =
$enumDecodeNullable(_$ProxiesSortTypeEnumMap, json['proxiesSortType']) ?? $enumDecodeNullable(_$ProxiesSortTypeEnumMap, json['proxiesSortType']) ??
ProxiesSortType.none ProxiesSortType.none
..proxiesLayout =
$enumDecodeNullable(_$ProxiesLayoutEnumMap, json['proxiesLayout']) ??
ProxiesLayout.standard
..isMinimizeOnExit = json['isMinimizeOnExit'] as bool? ?? true ..isMinimizeOnExit = json['isMinimizeOnExit'] as bool? ?? true
..isAccessControl = json['isAccessControl'] as bool? ?? false ..isAccessControl = json['isAccessControl'] as bool? ?? false
..accessControl = ..accessControl =
@@ -33,8 +36,6 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true ..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
..isCompatible = json['isCompatible'] as bool? ?? true ..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? false
..onlyProxy = json['onlyProxy'] as bool? ?? false ..onlyProxy = json['onlyProxy'] as bool? ?? false
..prueBlack = json['prueBlack'] as bool? ?? false ..prueBlack = json['prueBlack'] as bool? ?? false
..isCloseConnections = json['isCloseConnections'] as bool? ?? false ..isCloseConnections = json['isCloseConnections'] as bool? ?? false
@@ -44,12 +45,15 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..proxyCardType = ..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ?? $enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand ProxyCardType.expand
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2
..testUrl = ..testUrl =
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204' json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204'
..isExclude = json['isExclude'] as bool? ?? false ..isExclude = json['isExclude'] as bool? ?? false
..windowProps = ..windowProps =
WindowProps.fromJson(json['windowProps'] as Map<String, dynamic>?); WindowProps.fromJson(json['windowProps'] as Map<String, dynamic>?)
..vpnProps = VpnProps.fromJson(json['vpnProps'] as Map<String, dynamic>?)
..desktopProps =
DesktopProps.fromJson(json['desktopProps'] as Map<String, dynamic>?)
..showLabel = json['showLabel'] as bool? ?? false;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles, 'profiles': instance.profiles,
@@ -62,6 +66,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'locale': instance.locale, 'locale': instance.locale,
'primaryColor': instance.primaryColor, 'primaryColor': instance.primaryColor,
'proxiesSortType': _$ProxiesSortTypeEnumMap[instance.proxiesSortType]!, 'proxiesSortType': _$ProxiesSortTypeEnumMap[instance.proxiesSortType]!,
'proxiesLayout': _$ProxiesLayoutEnumMap[instance.proxiesLayout]!,
'isMinimizeOnExit': instance.isMinimizeOnExit, 'isMinimizeOnExit': instance.isMinimizeOnExit,
'isAccessControl': instance.isAccessControl, 'isAccessControl': instance.isAccessControl,
'accessControl': instance.accessControl, 'accessControl': instance.accessControl,
@@ -69,17 +74,17 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'isAnimateToPage': instance.isAnimateToPage, 'isAnimateToPage': instance.isAnimateToPage,
'isCompatible': instance.isCompatible, 'isCompatible': instance.isCompatible,
'autoCheckUpdate': instance.autoCheckUpdate, 'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
'onlyProxy': instance.onlyProxy, 'onlyProxy': instance.onlyProxy,
'prueBlack': instance.prueBlack, 'prueBlack': instance.prueBlack,
'isCloseConnections': instance.isCloseConnections, 'isCloseConnections': instance.isCloseConnections,
'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!, 'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!,
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!, 'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
'proxiesColumns': instance.proxiesColumns,
'test-url': instance.testUrl, 'test-url': instance.testUrl,
'isExclude': instance.isExclude, 'isExclude': instance.isExclude,
'windowProps': instance.windowProps, 'windowProps': instance.windowProps,
'vpnProps': instance.vpnProps,
'desktopProps': instance.desktopProps,
'showLabel': instance.showLabel,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
@@ -94,6 +99,12 @@ const _$ProxiesSortTypeEnumMap = {
ProxiesSortType.name: 'name', ProxiesSortType.name: 'name',
}; };
const _$ProxiesLayoutEnumMap = {
ProxiesLayout.loose: 'loose',
ProxiesLayout.standard: 'standard',
ProxiesLayout.tight: 'tight',
};
const _$ProxiesTypeEnumMap = { const _$ProxiesTypeEnumMap = {
ProxiesType.tab: 'tab', ProxiesType.tab: 'tab',
ProxiesType.list: 'list', ProxiesType.list: 'list',
@@ -117,6 +128,8 @@ _$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) =>
?.map((e) => e as String) ?.map((e) => e as String)
.toList() ?? .toList() ??
const [], const [],
sort: $enumDecodeNullable(_$AccessSortTypeEnumMap, json['sort']) ??
AccessSortType.none,
isFilterSystemApp: json['isFilterSystemApp'] as bool? ?? true, isFilterSystemApp: json['isFilterSystemApp'] as bool? ?? true,
); );
@@ -125,6 +138,7 @@ Map<String, dynamic> _$$AccessControlImplToJson(_$AccessControlImpl instance) =>
'mode': _$AccessControlModeEnumMap[instance.mode]!, 'mode': _$AccessControlModeEnumMap[instance.mode]!,
'acceptList': instance.acceptList, 'acceptList': instance.acceptList,
'rejectList': instance.rejectList, 'rejectList': instance.rejectList,
'sort': _$AccessSortTypeEnumMap[instance.sort]!,
'isFilterSystemApp': instance.isFilterSystemApp, 'isFilterSystemApp': instance.isFilterSystemApp,
}; };
@@ -133,6 +147,12 @@ const _$AccessControlModeEnumMap = {
AccessControlMode.rejectSelected: 'rejectSelected', AccessControlMode.rejectSelected: 'rejectSelected',
}; };
const _$AccessSortTypeEnumMap = {
AccessSortType.none: 'none',
AccessSortType.name: 'name',
AccessSortType.time: 'time',
};
_$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) => _$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
_$CoreStateImpl( _$CoreStateImpl(
accessControl: json['accessControl'] == null accessControl: json['accessControl'] == null
@@ -140,6 +160,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
: AccessControl.fromJson( : AccessControl.fromJson(
json['accessControl'] as Map<String, dynamic>), json['accessControl'] as Map<String, dynamic>),
currentProfileName: json['currentProfileName'] as String, currentProfileName: json['currentProfileName'] as String,
enable: json['enable'] as bool,
allowBypass: json['allowBypass'] as bool, allowBypass: json['allowBypass'] as bool,
systemProxy: json['systemProxy'] as bool, systemProxy: json['systemProxy'] as bool,
mixedPort: (json['mixedPort'] as num).toInt(), mixedPort: (json['mixedPort'] as num).toInt(),
@@ -150,6 +171,7 @@ Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'accessControl': instance.accessControl, 'accessControl': instance.accessControl,
'currentProfileName': instance.currentProfileName, 'currentProfileName': instance.currentProfileName,
'enable': instance.enable,
'allowBypass': instance.allowBypass, 'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy, 'systemProxy': instance.systemProxy,
'mixedPort': instance.mixedPort, 'mixedPort': instance.mixedPort,
@@ -171,3 +193,27 @@ Map<String, dynamic> _$$WindowPropsImplToJson(_$WindowPropsImpl instance) =>
'top': instance.top, 'top': instance.top,
'left': instance.left, 'left': instance.left,
}; };
_$VpnPropsImpl _$$VpnPropsImplFromJson(Map<String, dynamic> json) =>
_$VpnPropsImpl(
enable: json['enable'] as bool? ?? true,
systemProxy: json['systemProxy'] as bool? ?? false,
allowBypass: json['allowBypass'] as bool? ?? true,
);
Map<String, dynamic> _$$VpnPropsImplToJson(_$VpnPropsImpl instance) =>
<String, dynamic>{
'enable': instance.enable,
'systemProxy': instance.systemProxy,
'allowBypass': instance.allowBypass,
};
_$DesktopPropsImpl _$$DesktopPropsImplFromJson(Map<String, dynamic> json) =>
_$DesktopPropsImpl(
systemProxy: json['systemProxy'] as bool? ?? true,
);
Map<String, dynamic> _$$DesktopPropsImplToJson(_$DesktopPropsImpl instance) =>
<String, dynamic>{
'systemProxy': instance.systemProxy,
};

View File

@@ -23,6 +23,7 @@ mixin _$Package {
String get packageName => throw _privateConstructorUsedError; String get packageName => throw _privateConstructorUsedError;
String get label => throw _privateConstructorUsedError; String get label => throw _privateConstructorUsedError;
bool get isSystem => throw _privateConstructorUsedError; bool get isSystem => throw _privateConstructorUsedError;
int get firstInstallTime => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -34,7 +35,8 @@ abstract class $PackageCopyWith<$Res> {
factory $PackageCopyWith(Package value, $Res Function(Package) then) = factory $PackageCopyWith(Package value, $Res Function(Package) then) =
_$PackageCopyWithImpl<$Res, Package>; _$PackageCopyWithImpl<$Res, Package>;
@useResult @useResult
$Res call({String packageName, String label, bool isSystem}); $Res call(
{String packageName, String label, bool isSystem, int firstInstallTime});
} }
/// @nodoc /// @nodoc
@@ -53,6 +55,7 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package>
Object? packageName = null, Object? packageName = null,
Object? label = null, Object? label = null,
Object? isSystem = null, Object? isSystem = null,
Object? firstInstallTime = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
packageName: null == packageName packageName: null == packageName
@@ -67,6 +70,10 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package>
? _value.isSystem ? _value.isSystem
: isSystem // ignore: cast_nullable_to_non_nullable : isSystem // ignore: cast_nullable_to_non_nullable
as bool, as bool,
firstInstallTime: null == firstInstallTime
? _value.firstInstallTime
: firstInstallTime // ignore: cast_nullable_to_non_nullable
as int,
) as $Val); ) as $Val);
} }
} }
@@ -78,7 +85,8 @@ abstract class _$$PackageImplCopyWith<$Res> implements $PackageCopyWith<$Res> {
__$$PackageImplCopyWithImpl<$Res>; __$$PackageImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({String packageName, String label, bool isSystem}); $Res call(
{String packageName, String label, bool isSystem, int firstInstallTime});
} }
/// @nodoc /// @nodoc
@@ -95,6 +103,7 @@ class __$$PackageImplCopyWithImpl<$Res>
Object? packageName = null, Object? packageName = null,
Object? label = null, Object? label = null,
Object? isSystem = null, Object? isSystem = null,
Object? firstInstallTime = null,
}) { }) {
return _then(_$PackageImpl( return _then(_$PackageImpl(
packageName: null == packageName packageName: null == packageName
@@ -109,6 +118,10 @@ class __$$PackageImplCopyWithImpl<$Res>
? _value.isSystem ? _value.isSystem
: isSystem // ignore: cast_nullable_to_non_nullable : isSystem // ignore: cast_nullable_to_non_nullable
as bool, as bool,
firstInstallTime: null == firstInstallTime
? _value.firstInstallTime
: firstInstallTime // ignore: cast_nullable_to_non_nullable
as int,
)); ));
} }
} }
@@ -117,7 +130,10 @@ class __$$PackageImplCopyWithImpl<$Res>
@JsonSerializable() @JsonSerializable()
class _$PackageImpl implements _Package { class _$PackageImpl implements _Package {
const _$PackageImpl( const _$PackageImpl(
{required this.packageName, required this.label, required this.isSystem}); {required this.packageName,
required this.label,
required this.isSystem,
required this.firstInstallTime});
factory _$PackageImpl.fromJson(Map<String, dynamic> json) => factory _$PackageImpl.fromJson(Map<String, dynamic> json) =>
_$$PackageImplFromJson(json); _$$PackageImplFromJson(json);
@@ -128,10 +144,12 @@ class _$PackageImpl implements _Package {
final String label; final String label;
@override @override
final bool isSystem; final bool isSystem;
@override
final int firstInstallTime;
@override @override
String toString() { String toString() {
return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem)'; return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem, firstInstallTime: $firstInstallTime)';
} }
@override @override
@@ -143,12 +161,15 @@ class _$PackageImpl implements _Package {
other.packageName == packageName) && other.packageName == packageName) &&
(identical(other.label, label) || other.label == label) && (identical(other.label, label) || other.label == label) &&
(identical(other.isSystem, isSystem) || (identical(other.isSystem, isSystem) ||
other.isSystem == isSystem)); other.isSystem == isSystem) &&
(identical(other.firstInstallTime, firstInstallTime) ||
other.firstInstallTime == firstInstallTime));
} }
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => Object.hash(runtimeType, packageName, label, isSystem); int get hashCode =>
Object.hash(runtimeType, packageName, label, isSystem, firstInstallTime);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@@ -168,7 +189,8 @@ abstract class _Package implements Package {
const factory _Package( const factory _Package(
{required final String packageName, {required final String packageName,
required final String label, required final String label,
required final bool isSystem}) = _$PackageImpl; required final bool isSystem,
required final int firstInstallTime}) = _$PackageImpl;
factory _Package.fromJson(Map<String, dynamic> json) = _$PackageImpl.fromJson; factory _Package.fromJson(Map<String, dynamic> json) = _$PackageImpl.fromJson;
@@ -179,6 +201,8 @@ abstract class _Package implements Package {
@override @override
bool get isSystem; bool get isSystem;
@override @override
int get firstInstallTime;
@override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$PackageImplCopyWith<_$PackageImpl> get copyWith => _$$PackageImplCopyWith<_$PackageImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;

View File

@@ -11,6 +11,7 @@ _$PackageImpl _$$PackageImplFromJson(Map<String, dynamic> json) =>
packageName: json['packageName'] as String, packageName: json['packageName'] as String,
label: json['label'] as String, label: json['label'] as String,
isSystem: json['isSystem'] as bool, isSystem: json['isSystem'] as bool,
firstInstallTime: (json['firstInstallTime'] as num).toInt(),
); );
Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) => Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) =>
@@ -18,4 +19,5 @@ Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) =>
'packageName': instance.packageName, 'packageName': instance.packageName,
'label': instance.label, 'label': instance.label,
'isSystem': instance.isSystem, 'isSystem': instance.isSystem,
'firstInstallTime': instance.firstInstallTime,
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ class Package with _$Package {
required String packageName, required String packageName,
required String label, required String label,
required bool isSystem, required bool isSystem,
required int firstInstallTime,
}) = _Package; }) = _Package;
factory Package.fromJson(Map<String, Object?> json) => factory Package.fromJson(Map<String, Object?> json) =>

View File

@@ -1,7 +1,10 @@
import 'package:collection/collection.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:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lpinyin/lpinyin.dart';
part 'generated/selector.freezed.dart'; part 'generated/selector.freezed.dart';
@@ -34,10 +37,18 @@ class ProfilesSelectorState with _$ProfilesSelectorState {
const factory ProfilesSelectorState({ const factory ProfilesSelectorState({
required List<Profile> profiles, required List<Profile> profiles,
required String? currentProfileId, required String? currentProfileId,
required ViewMode viewMode, required int columns,
}) = _ProfilesSelectorState; }) = _ProfilesSelectorState;
} }
@freezed
class NetworkDetectionState with _$NetworkDetectionState {
const factory NetworkDetectionState({
required bool isTesting,
required IpInfo? ipInfo,
}) = _NetworkDetectionState;
}
@freezed @freezed
class ApplicationSelectorState with _$ApplicationSelectorState { class ApplicationSelectorState with _$ApplicationSelectorState {
const factory ApplicationSelectorState({ const factory ApplicationSelectorState({
@@ -53,7 +64,9 @@ class TrayContainerSelectorState with _$TrayContainerSelectorState {
const factory TrayContainerSelectorState({ const factory TrayContainerSelectorState({
required Mode mode, required Mode mode,
required bool autoLaunch, required bool autoLaunch,
required bool isRun, required bool systemProxy,
required bool tunEnable,
required bool isStart,
required String? locale, required String? locale,
}) = _TrayContainerSelectorState; }) = _TrayContainerSelectorState;
} }
@@ -132,17 +145,41 @@ class MoreToolsSelectorState with _$MoreToolsSelectorState {
@freezed @freezed
class PackageListSelectorState with _$PackageListSelectorState { class PackageListSelectorState with _$PackageListSelectorState {
const factory PackageListSelectorState({ const factory PackageListSelectorState({
required List<Package> packages,
required AccessControl accessControl, required AccessControl accessControl,
required bool isAccessControl, required bool isAccessControl,
}) = _PackageListSelectorState; }) = _PackageListSelectorState;
} }
@freezed extension PackageListSelectorStateExt on PackageListSelectorState {
class ColumnsSelectorState with _$ColumnsSelectorState { List<Package> getList(List<String> selectedList) {
const factory ColumnsSelectorState({ final isFilterSystemApp = accessControl.isFilterSystemApp;
required int columns, final sort = accessControl.sort;
required ViewMode viewMode, return packages
}) = _ColumnsSelectorState; .where((item) => isFilterSystemApp ? item.isSystem == false : true)
.sorted(
(a, b) {
return switch (sort) {
AccessSortType.none => 0,
AccessSortType.name => other.sortByChar(
PinyinHelper.getPinyin(a.label),
PinyinHelper.getPinyin(b.label),
),
AccessSortType.time =>
a.firstInstallTime.compareTo(b.firstInstallTime),
};
},
).sorted(
(a, b) {
final isSelectA = selectedList.contains(a.packageName);
final isSelectB = selectedList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
},
);
}
} }
@freezed @freezed
@@ -153,11 +190,49 @@ class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState {
}) = _ProxiesListHeaderSelectorState; }) = _ProxiesListHeaderSelectorState;
} }
@freezed @freezed
class ProxiesActionsState with _$ProxiesActionsState { class ProxiesActionsState with _$ProxiesActionsState {
const factory ProxiesActionsState({ const factory ProxiesActionsState({
required bool isCurrent, required bool isCurrent,
required bool hasProvider, required bool hasProvider,
}) = _ProxiesActionsState; }) = _ProxiesActionsState;
} }
@freezed
class AutoLaunchState with _$AutoLaunchState {
const factory AutoLaunchState({
required bool isAutoLaunch,
required bool isOpenTun,
}) = _AutoLaunchState;
}
@freezed
class ProxyState with _$ProxyState {
const factory ProxyState({
required bool isStart,
required bool systemProxy,
required int port,
}) = _ProxyState;
}
@freezed
class ClashConfigState with _$ClashConfigState {
const factory ClashConfigState({
required int mixedPort,
required bool allowLan,
required bool ipv6,
required String geodataLoader,
required LogLevel logLevel,
required String externalController,
required Mode mode,
required FindProcessMode findProcessMode,
required int keepAliveInterval,
required bool unifiedDelay,
required bool tcpConcurrent,
required Tun tun,
required Dns dns,
required GeoXMap geoXUrl,
required List<String> rules,
required String? globalRealUa,
}) = _ClashConfigState;
}

View File

@@ -13,20 +13,6 @@ typedef OnSelected = void Function(int index);
class HomePage extends StatelessWidget { class HomePage extends StatelessWidget {
const HomePage({super.key}); const HomePage({super.key});
_navigationBarContainer({
required BuildContext context,
required Widget child,
}) {
// if (!system.isDesktop) return child;
return Container(
padding: const EdgeInsets.all(16).copyWith(
right: 0,
),
color: context.colorScheme.surface,
child: child,
);
}
_getNavigationBar({ _getNavigationBar({
required BuildContext context, required BuildContext context,
required ViewMode viewMode, required ViewMode viewMode,
@@ -47,62 +33,78 @@ class HomePage extends StatelessWidget {
selectedIndex: currentIndex, selectedIndex: currentIndex,
); );
} }
final extended = viewMode == ViewMode.desktop; return LayoutBuilder(
return _navigationBarContainer( builder: (_, container) {
context: context, return Material(
child: NavigationRail( color: context.colorScheme.surfaceContainer,
groupAlignment: -0.8, child: Container(
selectedIconTheme: IconThemeData( padding: const EdgeInsets.symmetric(
color: context.colorScheme.onSurfaceVariant, vertical: 16,
),
unselectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
selectedLabelTextStyle: context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
unselectedLabelTextStyle: context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
extended: extended,
minExtendedWidth: 200,
selectedIndex: currentIndex,
labelType: extended
? NavigationRailLabelType.none
: NavigationRailLabelType.selected,
),
);
return NavigationRail(
groupAlignment: -0.95,
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
), ),
) height: container.maxHeight,
.toList(), child: Column(
onDestinationSelected: globalState.appController.toPage, children: [
extended: extended, Expanded(
minExtendedWidth: 172, child: SingleChildScrollView(
selectedIndex: currentIndex, child: IntrinsicHeight(
labelType: extended child: Selector<Config, bool>(
? NavigationRailLabelType.none selector: (_, config) => config.showLabel,
: NavigationRailLabelType.selected, builder: (_, showLabel, __) {
return NavigationRail(
backgroundColor:
context.colorScheme.surfaceContainer,
selectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
unselectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
selectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
unselectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
),
)
.toList(),
onDestinationSelected:
globalState.appController.toPage,
extended: false,
selectedIndex: currentIndex,
labelType: showLabel
? NavigationRailLabelType.all
: NavigationRailLabelType.none,
);
},
),
),
),
),
const SizedBox(
height: 16,
),
IconButton(
onPressed: () {
final config = globalState.appController.config;
config.showLabel = !config.showLabel;
},
icon: const Icon(Icons.menu),
)
],
),
),
);
},
); );
} }

View File

@@ -48,6 +48,16 @@ class App {
}); });
} }
Future<List<String>> getChinaPackageNames() async {
final packageNamesString =
await methodChannel.invokeMethod<String>("getChinaPackageNames");
return Isolate.run<List<String>>(() {
final List<dynamic> packageNamesRaw =
packageNamesString != null ? json.decode(packageNamesString) : [];
return packageNamesRaw.map((e) => e.toString()).toList();
});
}
Future<bool> openFile(String path) async { Future<bool> openFile(String path) async {
return await methodChannel.invokeMethod<bool>("openFile", { return await methodChannel.invokeMethod<bool>("openFile", {
"path": path, "path": path,

29
lib/plugins/service.dart Normal file
View File

@@ -0,0 +1,29 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/services.dart';
class Service {
static Service? _instance;
late MethodChannel methodChannel;
ReceivePort? receiver;
Service._internal() {
methodChannel = const MethodChannel("service");
}
factory Service() {
_instance ??= Service._internal();
return _instance!;
}
Future<bool?> init() async {
return await methodChannel.invokeMethod<bool>("init");
}
Future<bool?> destroy() async {
return await methodChannel.invokeMethod<bool>("destroy");
}
}
final service = Platform.isAndroid ? Service() : null;

View File

@@ -4,22 +4,18 @@ import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
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/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:proxy/proxy_platform_interface.dart';
class Proxy extends ProxyPlatform { class Vpn {
static Proxy? _instance; static Vpn? _instance;
late MethodChannel methodChannel; late MethodChannel methodChannel;
ReceivePort? receiver; ReceivePort? receiver;
ServiceMessageListener? _serviceMessageHandler; ServiceMessageListener? _serviceMessageHandler;
Proxy._internal() { Vpn._internal() {
methodChannel = const MethodChannel("proxy"); methodChannel = const MethodChannel("vpn");
methodChannel.setMethodCallHandler((call) async { methodChannel.setMethodCallHandler((call) async {
switch (call.method) { switch (call.method) {
case "started": case "started":
@@ -32,36 +28,21 @@ class Proxy extends ProxyPlatform {
}); });
} }
factory Proxy() { factory Vpn() {
_instance ??= Proxy._internal(); _instance ??= Vpn._internal();
return _instance!; return _instance!;
} }
Future<bool?> initService() async { Future<bool?> startVpn(port) async {
return await methodChannel.invokeMethod<bool>("initService");
}
handleStop() {
globalState.stopSystemProxy();
}
@override
Future<bool?> startProxy(port) async {
final state = clashCore.getState(); final state = clashCore.getState();
return await methodChannel.invokeMethod<bool>("startProxy", { return await methodChannel.invokeMethod<bool>("start", {
'port': state.mixedPort, 'port': state.mixedPort,
'args': json.encode(state), 'args': json.encode(state),
}); });
} }
@override Future<bool?> stopVpn() async {
Future<bool?> stopProxy() async { return await methodChannel.invokeMethod<bool>("stop");
clashCore.stopTun();
final isStop = await methodChannel.invokeMethod<bool>("stopProxy");
if (isStop == true) {
startTime = null;
}
return isStop;
} }
Future<bool?> setProtect(int fd) async { Future<bool?> setProtect(int fd) async {
@@ -78,11 +59,7 @@ class Proxy extends ProxyPlatform {
}); });
} }
bool get isStart => startTime != null && startTime!.isBeforeNow;
onStarted(int? fd) { onStarted(int? fd) {
debugPrint("onStarted ==> $fd");
if (fd == null) return;
if (receiver != null) { if (receiver != null) {
receiver!.close(); receiver!.close();
receiver == null; receiver == null;
@@ -91,11 +68,7 @@ class Proxy extends ProxyPlatform {
receiver!.listen((message) { receiver!.listen((message) {
_handleServiceMessage(message); _handleServiceMessage(message);
}); });
clashCore.startTun(fd, receiver!.sendPort.nativePort); clashCore.startTun(fd ?? 0, receiver!.sendPort.nativePort);
}
updateStartTime() {
startTime = clashCore.getRunTime();
} }
setServiceMessageHandler(ServiceMessageListener serviceMessageListener) { setServiceMessageHandler(ServiceMessageListener serviceMessageListener) {
@@ -104,7 +77,6 @@ class Proxy extends ProxyPlatform {
_handleServiceMessage(String message) { _handleServiceMessage(String message) {
final m = ServiceMessage.fromJson(json.decode(message)); final m = ServiceMessage.fromJson(json.decode(message));
debugPrint(m.toString());
switch (m.type) { switch (m.type) {
case ServiceMessageType.protect: case ServiceMessageType.protect:
_serviceMessageHandler?.onProtect(Fd.fromJson(m.data)); _serviceMessageHandler?.onProtect(Fd.fromJson(m.data));
@@ -118,4 +90,4 @@ class Proxy extends ProxyPlatform {
} }
} }
final proxy = Platform.isAndroid ? Proxy() : null; final vpn = Platform.isAndroid ? Vpn() : null;

View File

@@ -3,7 +3,8 @@ import 'dart:io';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/plugins/proxy.dart'; import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/plugins/vpn.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';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -18,14 +19,18 @@ class GlobalState {
Timer? timer; Timer? timer;
Timer? groupsUpdateTimer; Timer? groupsUpdateTimer;
var isVpnService = false; var isVpnService = false;
var autoRun = false;
late PackageInfo packageInfo; late PackageInfo packageInfo;
Function? updateCurrentDelayDebounce; Function? updateCurrentDelayDebounce;
PageController? pageController; PageController? pageController;
DateTime? startTime;
final navigatorKey = GlobalKey<NavigatorState>(); final navigatorKey = GlobalKey<NavigatorState>();
late AppController appController; late AppController appController;
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey(); GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
List<Function> updateFunctionLists = []; List<Function> updateFunctionLists = [];
bool get isStart => startTime != null && startTime!.isBeforeNow;
startListenUpdate() { startListenUpdate() {
if (timer != null && timer!.isActive == true) return; if (timer != null && timer!.isActive == true) return;
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
@@ -65,23 +70,32 @@ class GlobalState {
appState.versionInfo = clashCore.getVersionInfo(); appState.versionInfo = clashCore.getVersionInfo();
} }
Future<void> startSystemProxy({ handleStart({
required AppState appState,
required Config config, required Config config,
required ClashConfig clashConfig, required ClashConfig clashConfig,
}) async { }) async {
if (!globalState.isVpnService && Platform.isAndroid) { clashCore.start();
await proxy?.initService(); if (globalState.isVpnService) {
} else { await vpn?.startVpn(clashConfig.mixedPort);
await proxyManager.startProxy( startListenUpdate();
port: clashConfig.mixedPort, return;
);
} }
startTime ??= DateTime.now();
await service?.init();
startListenUpdate(); startListenUpdate();
} }
Future<void> stopSystemProxy() async { updateStartTime() {
await proxyManager.stopProxy(); startTime = clashCore.getRunTime();
}
handleStop() async {
clashCore.stop();
if (Platform.isAndroid) {
clashCore.stopTun();
}
await service?.destroy();
startTime = null;
stopListenUpdate(); stopListenUpdate();
} }
@@ -116,12 +130,14 @@ class GlobalState {
); );
clashCore.setState( clashCore.setState(
CoreState( CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null, accessControl: config.isAccessControl ? config.accessControl : null,
allowBypass: config.allowBypass, allowBypass: config.vpnProps.allowBypass,
systemProxy: config.systemProxy, systemProxy: config.vpnProps.systemProxy,
mixedPort: clashConfig.mixedPort, mixedPort: clashConfig.mixedPort,
onlyProxy: config.onlyProxy, onlyProxy: config.onlyProxy,
currentProfileName: config.currentProfile?.label ?? config.currentProfileId ?? "", currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
), ),
); );
} }
@@ -207,7 +223,7 @@ class GlobalState {
}) { }) {
final traffic = clashCore.getTraffic(); final traffic = clashCore.getTraffic();
if (Platform.isAndroid && isVpnService == true) { if (Platform.isAndroid && isVpnService == true) {
proxy?.startForeground( vpn?.startForeground(
title: clashCore.getState().currentProfileName, title: clashCore.getState().currentProfileName,
content: "$traffic", content: "$traffic",
); );

View File

@@ -1,5 +1,6 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'text.dart'; import 'text.dart';
@@ -29,12 +30,13 @@ class InfoHeader extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Flexible(
flex: 1,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.max,
children: [ children: [
if (info.iconData != null) ...[ if (info.iconData != null) ...[
Icon( Icon(
@@ -46,6 +48,7 @@ class InfoHeader extends StatelessWidget {
), ),
], ],
Flexible( Flexible(
flex: 1,
child: TooltipText( child: TooltipText(
text: Text( text: Text(
info.label, info.label,
@@ -58,6 +61,9 @@ class InfoHeader extends StatelessWidget {
], ],
), ),
), ),
const SizedBox(
width: 8,
),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@@ -155,6 +161,18 @@ class CommonCard extends StatelessWidget {
], ],
); );
} }
if (selectWidget != null && isSelected) {
final List<Widget> children = [];
children.add(childWidget);
children.add(
Positioned.fill(
child: selectWidget!,
),
);
childWidget = Stack(
children: children,
);
}
return OutlinedButton( return OutlinedButton(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
style: ButtonStyle( style: ButtonStyle(
@@ -172,25 +190,7 @@ class CommonCard extends StatelessWidget {
), ),
), ),
onPressed: onPressed, onPressed: onPressed,
child: Builder( child: childWidget,
builder: (_) {
if (selectWidget == null) {
return childWidget;
}
List<Widget> children = [];
children.add(childWidget);
if (isSelected) {
children.add(
Positioned.fill(
child: selectWidget!,
),
);
}
return Stack(
children: children,
);
},
),
); );
} }
} }

View File

@@ -1,10 +1,12 @@
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.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/plugins/proxy.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:provider/provider.dart'; import 'package:provider/provider.dart';
import '../common/function.dart';
class ClashContainer extends StatefulWidget { class ClashContainer extends StatefulWidget {
final Widget child; final Widget child;
@@ -19,12 +21,49 @@ class ClashContainer extends StatefulWidget {
class _ClashContainerState extends State<ClashContainer> class _ClashContainerState extends State<ClashContainer>
with AppMessageListener { with AppMessageListener {
Function? updateClashConfigDebounce;
Widget _updateContainer(Widget child) {
return Selector<ClashConfig, ClashConfigState>(
selector: (_, clashConfig) => ClashConfigState(
mixedPort: clashConfig.mixedPort,
allowLan: clashConfig.allowLan,
ipv6: clashConfig.ipv6,
logLevel: clashConfig.logLevel,
geodataLoader: clashConfig.geodataLoader,
externalController: clashConfig.externalController,
mode: clashConfig.mode,
findProcessMode: clashConfig.findProcessMode,
keepAliveInterval: clashConfig.keepAliveInterval,
unifiedDelay: clashConfig.unifiedDelay,
tcpConcurrent: clashConfig.tcpConcurrent,
tun: clashConfig.tun,
dns: clashConfig.dns,
geoXUrl: clashConfig.geoXUrl,
rules: clashConfig.rules,
globalRealUa: clashConfig.globalRealUa,
),
builder: (__, state, child) {
if (updateClashConfigDebounce == null) {
updateClashConfigDebounce = debounce<Function()>(() async {
await globalState.appController.updateClashConfig();
});
} else {
updateClashConfigDebounce!();
}
return child!;
},
child: child,
);
}
Widget _updateCoreState(Widget child) { Widget _updateCoreState(Widget child) {
return Selector2<Config, ClashConfig, CoreState>( return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState( selector: (_, config, clashConfig) => CoreState(
accessControl: config.isAccessControl ? config.accessControl : null, accessControl: config.isAccessControl ? config.accessControl : null,
allowBypass: config.allowBypass, enable: config.vpnProps.enable,
systemProxy: config.systemProxy, allowBypass: config.vpnProps.allowBypass,
systemProxy: config.vpnProps.systemProxy,
mixedPort: clashConfig.mixedPort, mixedPort: clashConfig.mixedPort,
onlyProxy: config.onlyProxy, onlyProxy: config.onlyProxy,
currentProfileName: currentProfileName:
@@ -38,8 +77,12 @@ class _ClashContainerState extends State<ClashContainer>
); );
} }
_changeProfileHandle() { _changeProfile() async {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
if (globalState.autoRun) {
globalState.autoRun = false;
return;
}
final appController = globalState.appController; final appController = globalState.appController;
appController.appState.delayMap = {}; appController.appState.delayMap = {};
await appController.applyProfile(); await appController.applyProfile();
@@ -50,7 +93,7 @@ class _ClashContainerState extends State<ClashContainer>
return Selector<Config, String?>( return Selector<Config, String?>(
selector: (_, config) => config.currentProfileId, selector: (_, config) => config.currentProfileId,
builder: (__, state, child) { builder: (__, state, child) {
_changeProfileHandle(); _changeProfile();
return child!; return child!;
}, },
child: child, child: child,
@@ -61,7 +104,9 @@ class _ClashContainerState extends State<ClashContainer>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _changeProfileContainer( return _changeProfileContainer(
_updateCoreState( _updateCoreState(
widget.child, _updateContainer(
widget.child,
),
), ),
); );
} }
@@ -79,15 +124,20 @@ class _ClashContainerState extends State<ClashContainer>
} }
@override @override
void onDelay(Delay delay) { Future<void> onDelay(Delay delay) async {
final appController = globalState.appController; final appController = globalState.appController;
appController.setDelay(delay); appController.setDelay(delay);
super.onDelay(delay); super.onDelay(delay);
await globalState.appController.updateGroupDebounce();
} }
@override @override
void onLog(Log log) { void onLog(Log log) {
globalState.appController.appState.addLog(log); globalState.appController.appState.addLog(log);
if (log.logLevel == LogLevel.error) {
globalState.appController.showSnackBar(log.payload ?? '');
}
debugPrint("$log");
super.onLog(log); super.onLog(log);
} }
@@ -108,13 +158,4 @@ class _ClashContainerState extends State<ClashContainer>
appController.addCheckIpNumDebounce(); appController.addCheckIpNumDebounce();
super.onLoaded(providerName); super.onLoaded(providerName);
} }
@override
Future<void> onStarted(String runTime) async {
super.onStarted(runTime);
proxy?.updateStartTime();
final appController = globalState.appController;
await appController.applyProfile(isPrue: true);
appController.addCheckIpNumDebounce();
}
} }

View File

@@ -0,0 +1,37 @@
import 'package:fl_clash/common/proxy.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProxyContainer extends StatelessWidget {
final Widget child;
const ProxyContainer({super.key, required this.child});
_updateProxy(ProxyState proxyState) {
final isStart = proxyState.isStart;
final systemProxy = proxyState.systemProxy;
final port = proxyState.port;
if (isStart && systemProxy) {
proxy?.startProxy(port);
}else{
proxy?.stopProxy();
}
}
@override
Widget build(BuildContext context) {
return Selector3<AppState, Config, ClashConfig, ProxyState>(
selector: (_, appState, config, clashConfig) => ProxyState(
isStart: appState.isStart,
systemProxy: config.desktopProps.systemProxy,
port: clashConfig.mixedPort,
),
builder: (_, state, child) {
_updateProxy(state);
return child!;
},
child: child,
);
}
}

View File

@@ -109,7 +109,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
valueListenable: _actions, valueListenable: _actions,
builder: (_, actions, __) { builder: (_, actions, __) {
final realActions = final realActions =
actions.isNotEmpty ? actions : widget.actions; actions.isNotEmpty ? actions : widget.actions;
return AppBar( return AppBar(
centerTitle: false, centerTitle: false,
automaticallyImplyLeading: widget.automaticallyImplyLeading, automaticallyImplyLeading: widget.automaticallyImplyLeading,

74
lib/widgets/setting.dart Normal file
View File

@@ -0,0 +1,74 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
import 'card.dart';
class SettingInfoCard extends StatelessWidget {
final Info info;
final bool? isSelected;
final VoidCallback onPressed;
const SettingInfoCard(
this.info, {
super.key,
this.isSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
isSelected: isSelected,
onPressed: onPressed,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Icon(info.iconData),
),
const SizedBox(
width: 8,
),
Flexible(
child: Text(
info.label,
style: context.textTheme.bodyMedium,
),
),
],
),
),
);
}
}
class SettingTextCard extends StatelessWidget {
final String text;
final bool? isSelected;
final VoidCallback onPressed;
const SettingTextCard(
this.text, {
super.key,
this.isSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: onPressed,
isSelected: isSelected,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text,
style: context.textTheme.bodyMedium,
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:emoji_regex/emoji_regex.dart';
import '../state.dart'; import '../state.dart';
@@ -30,3 +31,63 @@ class TooltipText extends StatelessWidget {
); );
} }
} }
class EmojiText extends StatelessWidget {
final String text;
final TextStyle? style;
final int? maxLines;
final TextOverflow? overflow;
const EmojiText(
this.text, {
super.key,
this.maxLines,
this.overflow,
this.style,
});
List<TextSpan> _buildTextSpans(String emojis) {
final List<TextSpan> spans = [];
final matches = emojiRegex().allMatches(text);
int lastMatchEnd = 0;
for (final match in matches) {
if (match.start > lastMatchEnd) {
spans.add(
TextSpan(
text: text.substring(lastMatchEnd, match.start), style: style),
);
}
spans.add(
TextSpan(
text:match.group(0),
style: style?.copyWith(
fontFamily: "Twemoji",
),
),
);
lastMatchEnd = match.end;
}
if (lastMatchEnd < text.length) {
spans.add(
TextSpan(
text: text.substring(lastMatchEnd),
style: style,
),
);
}
return spans;
}
@override
Widget build(BuildContext context) {
return RichText(
maxLines: maxLines,
overflow: overflow ?? TextOverflow.clip,
text: TextSpan(
children: _buildTextSpans(text),
),
);
}
}

View File

@@ -24,13 +24,13 @@ class _TileContainerState extends State<TileContainer> with TileListener {
@override @override
void onStart() { void onStart() {
globalState.appController.updateSystemProxy(true); globalState.appController.updateStatus(true);
super.onStart(); super.onStart();
} }
@override @override
void onStop() { void onStop() {
globalState.appController.updateSystemProxy(false); globalState.appController.updateStatus(false);
super.onStop(); super.onStop();
} }

View File

@@ -52,92 +52,121 @@ class _TrayContainerState extends State<TrayContainer> with TrayListener {
); );
} }
updateMenu(TrayContainerSelectorState state) async { updateMenu(TrayContainerSelectorState state) {
if (!Platform.isLinux) { WidgetsBinding.instance.addPostFrameCallback((_) {
_updateOtherTray(); if (!Platform.isLinux) {
} _updateOtherTray();
List<MenuItem> menuItems = []; }
final showMenuItem = MenuItem( List<MenuItem> menuItems = [];
label: appLocalizations.show, final showMenuItem = MenuItem(
onClick: (_) { label: appLocalizations.show,
window?.show(); onClick: (_) {
}, window?.show();
); },
menuItems.add(showMenuItem);
menuItems.add(MenuItem.separator());
// for (final group in state.groups) {
// List<MenuItem> subMenuItems = [];
// final isCurrentGroup = group.name == state.currentGroupName;
// for (final proxy in group.all) {
// final isCurrentProxy = proxy.name == state.currentProxyName;
// subMenuItems.add(
// MenuItem.checkbox(
// label: proxy.name,
// checked: isCurrentGroup && isCurrentProxy,
// onClick: (_) {
// final config = globalState.appController.config;
// config.currentProfile?.groupName = group.name;
// config.currentProfile?.proxyName = proxy.name;
// config.update();
// globalState.appController.changeProxy();
// }),
// );
// }
// menuItems.add(
// MenuItem.submenu(
// label: group.name,
// submenu: Menu(
// items: subMenuItems,
// ),
// ),
// );
// }
// if (state.groups.isNotEmpty) {
// menuItems.add(MenuItem.separator());
// }
for (final mode in Mode.values) {
menuItems.add(
MenuItem.checkbox(
label: Intl.message(mode.name),
onClick: (_) {
globalState.appController.clashConfig.mode = mode;
},
checked: mode == state.mode,
),
); );
} menuItems.add(showMenuItem);
menuItems.add(MenuItem.separator()); final startMenuItem = MenuItem.checkbox(
final proxyMenuItem = MenuItem.checkbox( label: state.isStart ? appLocalizations.stop : appLocalizations.start,
label: appLocalizations.systemProxy, onClick: (_) async {
onClick: (_) async { globalState.appController.updateStatus(!state.isStart);
globalState.appController.updateSystemProxy(!state.isRun); },
}, checked: false,
checked: state.isRun, );
); menuItems.add(startMenuItem);
menuItems.add(proxyMenuItem); menuItems.add(MenuItem.separator());
final autoStartMenuItem = MenuItem.checkbox( // for (final group in state.groups) {
label: appLocalizations.autoLaunch, // List<MenuItem> subMenuItems = [];
onClick: (_) async { // final isCurrentGroup = group.name == state.currentGroupName;
globalState.appController.config.autoLaunch = // for (final proxy in group.all) {
!globalState.appController.config.autoLaunch; // final isCurrentProxy = proxy.name == state.currentProxyName;
}, // subMenuItems.add(
checked: state.autoLaunch, // MenuItem.checkbox(
); // label: proxy.name,
menuItems.add(autoStartMenuItem); // checked: isCurrentGroup && isCurrentProxy,
menuItems.add(MenuItem.separator()); // onClick: (_) {
final exitMenuItem = MenuItem( // final config = globalState.appController.config;
label: appLocalizations.exit, // config.currentProfile?.groupName = group.name;
onClick: (_) async { // config.currentProfile?.proxyName = proxy.name;
await globalState.appController.handleExit(); // config.update();
}, // globalState.appController.changeProxy();
); // }),
menuItems.add(exitMenuItem); // );
final menu = Menu(); // }
menu.items = menuItems; // menuItems.add(
trayManager.setContextMenu(menu); // MenuItem.submenu(
if (Platform.isLinux) { // label: group.name,
_updateLinuxTray(); // submenu: Menu(
} // items: subMenuItems,
// ),
// ),
// );
// }
// if (state.groups.isNotEmpty) {
// menuItems.add(MenuItem.separator());
// }
for (final mode in Mode.values) {
menuItems.add(
MenuItem.checkbox(
label: Intl.message(mode.name),
onClick: (_) {
globalState.appController.clashConfig.mode = mode;
},
checked: mode == state.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (state.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun =
clashConfig.tun.copyWith(enable: !state.tunEnable);
},
checked: state.tunEnable,
),
);
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.systemProxy,
onClick: (_) {
final config = globalState.appController.config;
config.desktopProps =
config.desktopProps.copyWith(systemProxy: !state.systemProxy);
},
checked: state.systemProxy,
),
);
menuItems.add(MenuItem.separator());
}
final autoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.autoLaunch,
onClick: (_) async {
globalState.appController.config.autoLaunch =
!globalState.appController.config.autoLaunch;
},
checked: state.autoLaunch,
);
menuItems.add(autoStartMenuItem);
menuItems.add(MenuItem.separator());
final exitMenuItem = MenuItem(
label: appLocalizations.exit,
onClick: (_) async {
await globalState.appController.handleExit();
},
);
menuItems.add(exitMenuItem);
final menu = Menu();
menu.items = menuItems;
trayManager.setContextMenu(menu);
if (Platform.isLinux) {
_updateLinuxTray();
}
});
} }
@override @override
@@ -147,13 +176,13 @@ class _TrayContainerState extends State<TrayContainer> with TrayListener {
TrayContainerSelectorState( TrayContainerSelectorState(
mode: clashConfig.mode, mode: clashConfig.mode,
autoLaunch: config.autoLaunch, autoLaunch: config.autoLaunch,
isRun: appState.runTime != null, isStart: appState.isStart,
locale: config.locale, locale: config.locale,
systemProxy: config.desktopProps.systemProxy,
tunEnable: clashConfig.tun.enable,
), ),
builder: (_, state, child) { builder: (_, state, child) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { updateMenu(state);
updateMenu(state);
});
return child!; return child!;
}, },
child: widget.child, child: widget.child,

View File

@@ -24,4 +24,5 @@ export 'fade_box.dart';
export 'app_state_container.dart'; export 'app_state_container.dart';
export 'text.dart'; export 'text.dart';
export 'connection_item.dart'; export 'connection_item.dart';
export 'builder.dart'; export 'builder.dart';
export 'setting.dart';

View File

@@ -20,11 +20,17 @@ class WindowContainer extends StatefulWidget {
} }
class _WindowContainerState extends State<WindowContainer> with WindowListener { class _WindowContainerState extends State<WindowContainer> with WindowListener {
Function? updateLaunchDebounce;
_autoLaunchContainer(Widget child) { _autoLaunchContainer(Widget child) {
return Selector<Config, bool>( return Selector2<Config, ClashConfig, AutoLaunchState>(
selector: (_, config) => config.autoLaunch, selector: (_, config, clashConfig) => AutoLaunchState(
builder: (_, isAutoLaunch, child) { isAutoLaunch: config.autoLaunch, isOpenTun: clashConfig.tun.enable),
autoLaunch?.updateStatus(isAutoLaunch); builder: (_, state, child) {
updateLaunchDebounce ??= debounce((AutoLaunchState state) {
autoLaunch?.updateStatus(state);
});
updateLaunchDebounce!([state]);
return child!; return child!;
}, },
child: child, child: child,
@@ -33,22 +39,7 @@ class _WindowContainerState extends State<WindowContainer> with WindowListener {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return _autoLaunchContainer(widget.child);
children: [
Column(
children: [
SizedBox(
height: kHeaderHeight,
),
Expanded(
flex: 1,
child: _autoLaunchContainer(widget.child),
),
],
),
const WindowHeader(),
],
);
} }
@override @override
@@ -98,6 +89,35 @@ class _WindowContainerState extends State<WindowContainer> with WindowListener {
} }
} }
class WindowHeaderContainer extends StatelessWidget {
final Widget child;
const WindowHeaderContainer({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Column(
children: [
SizedBox(
height: kHeaderHeight,
),
Expanded(
flex: 1,
child: child,
),
],
),
const WindowHeader(),
],
);
}
}
class WindowHeader extends StatefulWidget { class WindowHeader extends StatefulWidget {
const WindowHeader({super.key}); const WindowHeader({super.key});
@@ -188,7 +208,7 @@ class _WindowHeaderState extends State<WindowHeader> {
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
windowManager.close(); globalState.appController.handleBackOrExit();
}, },
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
), ),
@@ -214,7 +234,7 @@ class _WindowHeaderState extends State<WindowHeader> {
_updateMaximized(); _updateMaximized();
}, },
child: Container( child: Container(
color: context.colorScheme.surface, color: context.colorScheme.secondary.toSoft(),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
height: kHeaderHeight, height: kHeaderHeight,
), ),

View File

@@ -10,50 +10,22 @@ class Proxy extends ProxyPlatform {
@override @override
Future<bool?> startProxy(int port) async { Future<bool?> startProxy(int port) async {
bool? isStart = false; return switch (Platform.operatingSystem) {
switch (Platform.operatingSystem) { "macos" => await _startProxyWithMacos(port),
case "macos": "linux" => await _startProxyWithLinux(port),
isStart = await _startProxyWithMacos(port); "windows" => await ProxyPlatform.instance.startProxy(port),
break; String() => false,
case "linux": };
isStart = await _startProxyWithLinux(port);
break;
case "windows":
isStart = await ProxyPlatform.instance.startProxy(port);
break;
}
if (isStart == true) {
startTime = DateTime.now();
}
return isStart;
} }
@override @override
Future<bool?> stopProxy() async { Future<bool?> stopProxy() async {
bool? isStop = false; return switch (Platform.operatingSystem) {
switch (Platform.operatingSystem) { "macos" => await _stopProxyWithMacos(),
case "macos": "linux" => await _stopProxyWithLinux(),
isStop = await _stopProxyWithMacos(); "windows" => await ProxyPlatform.instance.stopProxy(),
break; String() => false,
case "linux": };
isStop = await _stopProxyWithLinux();
break;
case "windows":
isStop = await ProxyPlatform.instance.stopProxy();
break;
}
if (isStop == true) {
startTime = null;
}
return isStop;
}
@override
get startTime => ProxyPlatform.instance.startTime;
@override
set startTime(DateTime? dateTime) {
ProxyPlatform.instance.startTime = dateTime;
} }
Future<bool> _startProxyWithLinux(int port) async { Future<bool> _startProxyWithLinux(int port) async {
@@ -205,4 +177,3 @@ class Proxy extends ProxyPlatform {
return lines; return lines;
} }
} }

View File

@@ -18,10 +18,6 @@ class MethodChannelProxy extends ProxyPlatform {
@override @override
Future<bool?> stopProxy() async { Future<bool?> stopProxy() async {
final isStop = await methodChannel.invokeMethod<bool>("StopProxy"); return await methodChannel.invokeMethod<bool>("StopProxy");
if (isStop == true) {
startTime = null;
}
return isStop;
} }
} }

View File

@@ -20,8 +20,6 @@ abstract class ProxyPlatform extends PlatformInterface {
_instance = instance; _instance = instance;
} }
DateTime? startTime;
Future<bool?> startProxy(int port) { Future<bool?> startProxy(int port) {
throw UnimplementedError('startProxy() has not been implemented.'); throw UnimplementedError('startProxy() has not been implemented.');
} }

View File

@@ -193,14 +193,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
country_flags:
dependency: "direct main"
description:
name: country_flags
sha256: dbc4f76e7c801619b2d841023e0327191ba00663f1f1b4770394e9bc6632c444
url: "https://pub.dev"
source: hosted
version: "2.2.0"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@@ -249,6 +241,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.7.0" version: "1.7.0"
emoji_regex:
dependency: "direct main"
description:
name: emoji_regex
sha256: "3a25dd4d16f98b6f76dc37cc9ae49b8511891ac4b87beac9443a1e9f4634b6c7"
url: "https://pub.dev"
source: hosted
version: "0.0.5"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -541,22 +541,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.1" version: "4.3.1"
jovial_misc:
dependency: transitive
description:
name: jovial_misc
sha256: f6e64f789ee311025bb367be9c9afe9759f76dd8209070b7f38e735b5f529eb1
url: "https://pub.dev"
source: hosted
version: "0.8.5"
jovial_svg:
dependency: transitive
description:
name: jovial_svg
sha256: "000cafe183bdeba28f76d65bf93fc9b636d6f7eaa7e2a33e80c4345a28866ea8"
url: "https://pub.dev"
source: hosted
version: "1.1.21"
js: js:
dependency: transitive dependency: transitive
description: description:
@@ -629,6 +613,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
lpinyin:
dependency: "direct main"
description:
name: lpinyin
sha256: "0bb843363f1f65170efd09fbdfc760c7ec34fc6354f9fcb2f89e74866a0d814a"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -638,7 +630,7 @@ packages:
source: hosted source: hosted
version: "0.12.16+1" version: "0.12.16+1"
material_color_utilities: material_color_utilities:
dependency: "direct main" dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
@@ -789,14 +781,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@@ -1243,4 +1227,4 @@ packages:
version: "0.2.3" version: "0.2.3"
sdks: sdks:
dart: ">=3.4.0 <4.0.0" dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0" flutter: ">=3.22.3"

View File

@@ -1,9 +1,10 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.51+202408051 version: 0.8.55+202408251
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'
flutter: 3.22.3
dependencies: dependencies:
flutter: flutter:
@@ -37,13 +38,13 @@ dependencies:
image: ^4.1.7 image: ^4.1.7
webdav_client: ^1.2.2 webdav_client: ^1.2.2
dio: ^5.4.3+1 dio: ^5.4.3+1
country_flags: ^2.2.0
win32: ^5.5.1 win32: ^5.5.1
ffi: ^2.1.2 ffi: ^2.1.2
material_color_utilities: ^0.8.0
re_editor: ^0.3.1 re_editor: ^0.3.1
re_highlight: ^0.0.3 re_highlight: ^0.0.3
archive: ^3.6.1 archive: ^3.6.1
lpinyin: ^2.0.3
emoji_regex: ^0.0.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -58,8 +59,13 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/data/ - assets/data/
- assets/fonts/
- assets/images/ - assets/images/
- assets/images/avatars/ - assets/images/avatars/
fonts:
- family: Twemoji
fonts:
- asset: assets/fonts/Twemoji.Mozilla.ttf
ffigen: ffigen:
name: "ClashFFI" name: "ClashFFI"
output: 'lib/clash/generated/clash_ffi.dart' output: 'lib/clash/generated/clash_ffi.dart'

View File

@@ -2,7 +2,18 @@
import 'dart:io'; import 'dart:io';
void main() async { import 'package:fl_clash/common/other.dart';
import 'package:lpinyin/lpinyin.dart';
void main() {
print(PinyinHelper.getPinyin("ABC"));
print(PinyinHelper.getPinyin("阿里巴巴"));
print('a'.compareTo('B'));
print('A'.compareTo('B'));
}
startService() async {
// 定义服务器将要监听的地址和端口 // 定义服务器将要监听的地址和端口
final host = InternetAddress.anyIPv4; // 监听所有网络接口 final host = InternetAddress.anyIPv4; // 监听所有网络接口
const port = 8080; // 使用 8080 端口 const port = 8080; // 使用 8080 端口

View File

@@ -18,10 +18,6 @@ add_executable(${BINARY_NAME} WIN32
"runner.exe.manifest" "runner.exe.manifest"
) )
# add_executable(service
# "service.cpp"
# )
# Apply the standard set of build settings. This can be removed for applications # Apply the standard set of build settings. This can be removed for applications
# that need different build settings. # that need different build settings.
apply_standard_settings(${BINARY_NAME}) apply_standard_settings(${BINARY_NAME})

View File

@@ -1,152 +0,0 @@
#include <windows.h>
#include <tchar.h>
#include <string>
#define SERVICE_NAME _T("MyService")
SERVICE_STATUS g_ServiceStatus = {0};
SERVICE_STATUS_HANDLE g_StatusHandle = NULL;
HANDLE g_ServiceStopEvent = INVALID_HANDLE_VALUE;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv);
VOID WINAPI ServiceCtrlHandler(DWORD);
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam);
int _tmain(int argc, TCHAR *argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[] =
{
{SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION) ServiceMain},
{NULL, NULL}
};
if (StartServiceCtrlDispatcher(ServiceTable) == FALSE)
{
return GetLastError();
}
return 0;
}
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv)
{
DWORD Status = E_FAIL;
g_StatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME, ServiceCtrlHandler);
if (g_StatusHandle == NULL)
{
goto EXIT;
}
ZeroMemory(&g_ServiceStatus, sizeof(g_ServiceStatus));
g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
g_ServiceStatus.dwWin32ExitCode = 0;
g_ServiceStatus.dwServiceSpecificExitCode = 0;
g_ServiceStatus.dwCheckPoint = 0;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
g_ServiceStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (g_ServiceStopEvent == NULL)
{
g_ServiceStatus.dwControlsAccepted = 0;
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
g_ServiceStatus.dwWin32ExitCode = GetLastError();
g_ServiceStatus.dwCheckPoint = 1;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
goto EXIT;
}
g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
g_ServiceStatus.dwCheckPoint = 0;
g_ServiceStatus.dwWaitHint = 0;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
HANDLE hThread = CreateThread(NULL, 0, ServiceWorkerThread, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(g_ServiceStopEvent);
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
g_ServiceStatus.dwCheckPoint = 3;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
EXIT:
return;
}
VOID WINAPI ServiceCtrlHandler(DWORD CtrlCode)
{
switch(CtrlCode)
{
case SERVICE_CONTROL_STOP:
if (g_ServiceStatus.dwCurrentState != SERVICE_RUNNING)
break;
g_ServiceStatus.dwControlsAccepted = 0;
g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
g_ServiceStatus.dwWin32ExitCode = 0;
g_ServiceStatus.dwCheckPoint = 4;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceCtrlHandler: SetServiceStatus returned error"));
}
SetEvent(g_ServiceStopEvent);
break;
default:
break;
}
}
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
{
while(WaitForSingleObject(g_ServiceStopEvent, 0) != WAIT_OBJECT_0)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// 启动 "C:\path\to\your\executable.exe"
if(!CreateProcess(NULL, _T("C:\\path\\to\\your\\executable.exe"), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
OutputDebugString(_T("CreateProcess failed"));
}
// 等待进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
// 关闭进程和线程句柄
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
// 每隔一段时间检查一次这里设置为60秒
Sleep(60000);
}
return ERROR_SUCCESS;
}