Compare commits

..

1 Commits

Author SHA1 Message Date
chen08209
196a50aaf7 Fix android tile service issues 2024-08-01 23:51:00 +08:00
113 changed files with 3090 additions and 6604 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,18 @@
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.drawable.Drawable
import android.os.Build
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
import androidx.core.app.NotificationCompat
import java.net.URL
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.models.Metadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.InetAddress
import java.net.InetSocketAddress
suspend fun Drawable.getBase64(): String {
@@ -41,6 +31,7 @@ fun Metadata.getProtocol(): Int? {
return null
}
private val CHANNEL = "FlClash"
private val notificationId: Int = 1
fun String.getInetSocketAddress(): InetSocketAddress {
val url = URL("https://$this")
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
}

View File

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

View File

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

View File

@@ -6,16 +6,11 @@ import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.VpnService
import android.os.Build
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import com.follow.clash.GlobalState
@@ -24,7 +19,6 @@ import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Package
import com.follow.clash.models.Process
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
@@ -38,7 +32,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.net.InetSocketAddress
import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
@@ -46,7 +40,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var toast: Toast? = null
private lateinit var context: Context
private var context: Context? = null
private lateinit var channel: MethodChannel
@@ -54,78 +48,14 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var connectivity: ConnectivityManager? = null
private var vpnCallBack: (() -> Unit)? = null
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) {
scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -158,13 +88,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
"getPackages" -> {
scope.launch {
result.success(getPackagesToJson())
}
}
"getChinaPackageNames" -> {
scope.launch {
result.success(getChinaPackageNames())
result.success(getPackages())
}
}
@@ -183,7 +107,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
if (iconMap["default"] == null) {
iconMap["default"] =
context.packageManager?.defaultActivityIcon?.getBase64()
context?.packageManager?.defaultActivityIcon?.getBase64()
}
result.success(iconMap["default"])
return@launch
@@ -210,8 +134,12 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(null)
return@withContext
}
if (context == null) {
result.success(null)
return@withContext
}
if (connectivity == null) {
connectivity = context.getSystemService<ConnectivityManager>()
connectivity = context!!.getSystemService<ConnectivityManager>()
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
@@ -227,7 +155,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(null)
return@withContext
}
val packages = context.packageManager?.getPackagesForUid(uid)
val packages = context?.packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
@@ -252,43 +180,46 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private fun openFile(path: String) {
val file = File(path)
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileProvider",
file
)
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,
flags
context?.let {
val file = File(path)
val uri = FileProvider.getUriForFile(
it,
"${it.packageName}.fileProvider",
file
)
}
try {
activity?.startActivity(intent)
} catch (e: Exception) {
println(e)
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 = it.packageManager.queryIntentActivities(
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?) {
val am = getSystemService(context, ActivityManager::class.java)
if (context == null) return
val am = getSystemService(context!!, ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId
@@ -305,7 +236,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context.packageManager
val packageManager = context?.packageManager
if (iconMap[packageName] == null) {
iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -317,144 +248,32 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
return iconMap[packageName]
}
private fun getPackages(): List<Package> {
val packageManager = context.packageManager
if (packages.isNotEmpty()) return packages;
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != context.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
firstInstallTime = it.firstInstallTime
)
}?.let { packages.addAll(it) }
return packages;
}
private suspend fun getPackagesToJson(): String {
private suspend fun getPackages(): String {
return withContext(Dispatchers.Default) {
Gson().toJson(getPackages())
}
}
val packageManager = context?.packageManager
val packages: List<Package>? =
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != context?.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
private suspend fun getChinaPackageNames(): String {
return withContext(Dispatchers.Default) {
val packages: List<String> =
getPackages().map { it.packageName }.filter { isChinaPackage(it) }
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1
)
}
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() {
channel.invokeMethod("gc", null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity;
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
override fun onDetachedFromActivityForConfigChanges() {
@@ -469,25 +288,4 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
channel.invokeMethod("exit", 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

@@ -0,0 +1,220 @@
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

@@ -1,47 +0,0 @@
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

@@ -1,143 +0,0 @@
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

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

Binary file not shown.

View File

@@ -2,9 +2,20 @@ package main
import "C"
import (
"context"
"errors"
"math"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
ap "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"os"
"os/exec"
"path/filepath"
@@ -13,27 +24,43 @@ import (
"sync"
"syscall"
"time"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/batch"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/sniffer"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
)
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 {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
@@ -42,9 +69,9 @@ type ConfigExtendedParams struct {
}
type GenerateConfigParams struct {
ProfileId string `json:"profile-id"`
Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"`
ProfilePath *string `json:"profile-path"`
Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"`
}
type ChangeProxyParams struct {
@@ -66,19 +93,9 @@ type ExternalProvider struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicle-type"`
Count int `json:"count"`
Path string `json:"path"`
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))
func restartExecutable(execPath string) {
var err error
executor.Shutdown()
@@ -128,108 +145,26 @@ func removeFile(path string) error {
return nil
}
func getProfilePath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
}
func getProfileProvidersPath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "providers", id)
}
func getRawConfigWithId(id string) *config.RawConfig {
path := getProfilePath(id)
bytes, err := readFile(path)
if err != nil {
log.Errorln("profile is not exist")
func getRawConfigWithPath(path *string) *config.RawConfig {
if path == nil {
return config.DefaultRawConfig()
}
prof, err := config.UnmarshalRawConfig(bytes)
if err != nil {
log.Errorln("unmarshalRawConfig error %v", err)
return config.DefaultRawConfig()
}
for _, mapping := range prof.ProxyProvider {
value, exist := mapping["path"].(string)
if !exist {
continue
} else {
bytes, err := readFile(*path)
if err != nil {
log.Errorln("getProfile readFile error %v", err)
return config.DefaultRawConfig()
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
}
for _, mapping := range prof.RuleProvider {
value, exist := mapping["path"].(string)
if !exist {
continue
prof, err := config.UnmarshalRawConfig(bytes)
if err != nil {
log.Errorln("getProfile UnmarshalRawConfig error %v", err)
return config.DefaultRawConfig()
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
}
return prof
}
func getExternalProvidersRaw() map[string]cp.Provider {
eps := make(map[string]cp.Provider)
for n, p := range tunnel.Providers() {
if p.VehicleType() != cp.Compatible {
eps[n] = p
}
}
for n, p := range tunnel.RuleProviders() {
if p.VehicleType() != cp.Compatible {
eps[n] = p
}
}
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")
return prof
}
}
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 {
prof := getRawConfigWithId(profileId)
func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig {
prof := getRawConfigWithPath(profilePath)
overwriteConfig(prof, cfg)
return prof
}
@@ -392,7 +327,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
targetConfig.SocksPort = 0
targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.FindProcessMode = patchConfig.FindProcessMode
targetConfig.AllowLan = patchConfig.AllowLan
@@ -423,65 +357,30 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
func patchConfig(general *config.General) {
log.Infoln("[Apply] patch")
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)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
inbound.SetAllowedIPs(general.LanAllowedIPs)
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
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.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel)
listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel)
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
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)
}
}()
}
}
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6
}
func patchSelectGroup() {
@@ -509,8 +408,12 @@ func patchSelectGroup() {
}
}
var applyLock sync.Mutex
func applyConfig() error {
cfg, err := config.ParseRawConfig(currentRawConfig)
applyLock.Lock()
defer applyLock.Unlock()
cfg, err := config.ParseRawConfig(currentConfig)
if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
@@ -522,13 +425,8 @@ func applyConfig() error {
} else {
closeConnections()
runtime.GC()
hub.UltraApplyConfig(cfg)
hub.UltraApplyConfig(cfg, true)
patchSelectGroup()
}
if isRunning {
updateListeners(cfg.General, cfg.Listeners)
hcCompatibleProvider(cfg.Providers)
}
externalProviders = getExternalProvidersRaw()
return err
}

View File

@@ -8,16 +8,10 @@ import (
bridge "core/dart-bridge"
"encoding/json"
"fmt"
"os"
"runtime"
"sort"
"sync"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
@@ -25,33 +19,23 @@ import (
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"golang.org/x/net/context"
"os"
"runtime"
"time"
"unsafe"
)
var currentRawConfig = config.DefaultRawConfig()
var currentConfig = config.DefaultRawConfig()
var configParams = ConfigExtendedParams{}
var externalProviders = map[string]cp.Provider{}
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()
}
var currentProfileName = ""
//export initClash
func initClash(homeDirStr *C.char) bool {
@@ -76,10 +60,10 @@ func restartClash() bool {
//export shutdownClash
func shutdownClash() bool {
stopListeners()
executor.Shutdown()
runtime.GC()
isInit = false
currentConfig = nil
return true
}
@@ -91,6 +75,16 @@ func forceGc() {
}()
}
//export setCurrentProfileName
func setCurrentProfileName(s *C.char) {
currentProfileName = C.GoString(s)
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
return C.CString(currentProfileName)
}
//export validateConfig
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
@@ -105,15 +99,11 @@ func validateConfig(s *C.char, port C.longlong) {
}()
}
var updateLock sync.Mutex
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
go func() {
updateLock.Lock()
defer updateLock.Unlock()
var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
@@ -121,8 +111,8 @@ func updateConfig(s *C.char, port C.longlong) {
return
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
currentRawConfig = prof
prof := decorationConfig(params.ProfilePath, params.Config)
currentConfig = prof
err = applyConfig()
if err != nil {
bridge.SendToPort(i, err.Error())
@@ -134,10 +124,34 @@ func updateConfig(s *C.char, port C.longlong) {
//export clearEffect
func clearEffect(s *C.char) {
id := C.GoString(s)
path := C.GoString(s)
go func() {
_ = removeFile(getProfilePath(id))
_ = removeFile(getProfileProvidersPath(id))
rawCfg := getRawConfigWithPath(&path)
for _, mapping := range rawCfg.RuleProvider {
schema := &ruleProviderSchema{}
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
if err := decoder.Decode(mapping, schema); err != nil {
return
}
if schema.Type == "http" {
_ = removeFile(constant.Path.Resolve(schema.Path))
}
}
for _, mapping := range rawCfg.ProxyProvider {
schema := &proxyProviderSchema{
HealthCheck: healthCheckSchema{
Lazy: true,
},
}
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
if err := decoder.Decode(mapping, schema); err != nil {
return
}
if schema.Type == "http" {
_ = removeFile(constant.Path.Resolve(schema.Path))
}
}
_ = removeFile(path)
}()
}
@@ -170,13 +184,10 @@ func changeProxy(s *C.char) {
if !ok {
return
}
if proxyName == "" {
selector.ForceSet(proxyName)
} else {
err = selector.Set(proxyName)
}
err = selector.Set(proxyName)
if err == nil {
log.Infoln("[SelectAble] %s selected %s", groupName, proxyName)
log.Infoln("[Selector] %s selected %s", groupName, proxyName)
}
}
@@ -219,16 +230,16 @@ func resetTraffic() {
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
b.Go(paramsString, func() (bool, error) {
go func() {
var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
return false, nil
return
}
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil {
return false, nil
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
@@ -245,7 +256,7 @@ func asyncTestDelay(s *C.char, port C.longlong) {
delayData.Value = -1
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return false, nil
return
}
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
@@ -253,14 +264,14 @@ func asyncTestDelay(s *C.char, port C.longlong) {
delayData.Value = -1
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return false, nil
return
}
delayData.Value = int32(delay)
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return false, nil
})
return
}()
}
//export getVersionInfo
@@ -334,67 +345,78 @@ func getProvider(name *C.char) *C.char {
//export getExternalProviders
func getExternalProviders() *C.char {
eps := make([]ExternalProvider, 0)
for _, p := range externalProviders {
externalProvider, err := toExternalProvider(p)
if err != nil {
continue
externalProviders := make([]ExternalProvider, 0)
providers := tunnel.Providers()
for n, p := range providers {
if p.VehicleType() != cp.Compatible {
p := p.(*provider.ProxySetProvider)
externalProviders = append(externalProviders, ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
UpdateAt: p.UpdatedAt,
})
}
eps = append(eps, *externalProvider)
}
sort.Sort(ExternalProviders(eps))
data, err := json.Marshal(eps)
for n, p := range tunnel.RuleProviders() {
if p.VehicleType() != cp.Compatible {
p := p.(*rp.RuleSetProvider)
externalProviders = append(externalProviders, ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
UpdateAt: p.UpdatedAt,
})
}
}
data, err := json.Marshal(externalProviders)
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export getExternalProvider
func getExternalProvider(name *C.char) *C.char {
externalProviderName := C.GoString(name)
externalProvider, exist := externalProviders[externalProviderName]
if !exist {
return C.CString("")
}
e, err := toExternalProvider(externalProvider)
if err != nil {
return C.CString("")
}
data, err := json.Marshal(e)
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export updateGeoData
func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) {
i := int64(port)
geoTypeString := C.GoString(geoType)
geoNameString := C.GoString(geoName)
providerNameString := C.GoString(providerName)
providerTypeString := C.GoString(providerType)
go func() {
switch geoTypeString {
switch providerTypeString {
case "Proxy":
providers := tunnel.Providers()
err := providers[providerNameString].Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "Rule":
providers := tunnel.RuleProviders()
err := providers[providerNameString].Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "MMDB":
err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString))
err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "ASN":
err := updater.UpdateASN(constant.Path.Resolve(geoNameString))
err := updater.UpdateASN(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoIp":
err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString))
err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString))
err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
@@ -404,45 +426,6 @@ func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
}()
}
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, port C.longlong) {
i := int64(port)
providerNameString := C.GoString(providerName)
go func() {
externalProvider, exist := externalProviders[providerNameString]
if !exist {
bridge.SendToPort(i, "external provider is not exist")
return
}
err := externalProvider.Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export sideLoadExternalProvider
func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(data))
providerNameString := C.GoString(providerName)
go func() {
externalProvider, exist := externalProviders[providerNameString]
if !exist {
bridge.SendToPort(i, "external provider is not exist")
return
}
err := sideUpdateExternalProvider(externalProvider, bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api)
@@ -480,7 +463,7 @@ func init() {
Data: c,
})
}
executor.DefaultProviderLoadedHook = func(providerName string) {
executor.DefaultProxyProviderLoadedHook = func(providerName string) {
SendMessage(Message{
Type: LoadedMessage,
Data: providerName,

View File

@@ -14,7 +14,6 @@ type AccessControl struct {
}
type AndroidProps struct {
Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
@@ -22,9 +21,8 @@ type AndroidProps struct {
type State struct {
AndroidProps
CurrentProfileName string `json:"currentProfileName"`
MixedPort int `json:"mixedPort"`
OnlyProxy bool `json:"onlyProxy"`
MixedPort int `json:"mixedPort"`
OnlyProxy bool `json:"onlyProxy"`
}
var state State

View File

@@ -7,15 +7,14 @@ import (
"core/platform"
t "core/tun"
"errors"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
)
var tunLock sync.Mutex
@@ -41,18 +40,6 @@ var fdMap FdMap
func startTUN(fd C.int, port C.longlong) {
i := int64(port)
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() {
tunLock.Lock()
defer tunLock.Unlock()
@@ -101,7 +88,6 @@ func getRunTime() *C.char {
//export stopTun
func stopTun() {
removeSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
@@ -139,7 +125,7 @@ func markSocket(fd Fd) {
var fdCounter int64 = 0
func initSocketHook() {
func init() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
@@ -173,7 +159,3 @@ func initSocketHook() {
})
}
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}

View File

@@ -67,6 +67,7 @@ func Start(fd int, gateway, portal, dns string) (io.Closer, error) {
for stack.TCP().SetDeadline(time.Time{}) == nil {
conn, err := stack.TCP().Accept()
if err != nil {
log.Errorln("Accept connection: %v", err)
continue
}
lAddr := conn.LocalAddr().(*net.TCPAddr)

View File

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

View File

@@ -100,6 +100,22 @@ class ClashCore {
);
}
setProfileName(String profileName) {
final profileNameChar = profileName.toNativeUtf8().cast<Char>();
clashFFI.setCurrentProfileName(
profileNameChar,
);
malloc.free(profileNameChar);
}
getProfileName() {
final currentProfileNameRaw = clashFFI.getCurrentProfileName();
final currentProfileName =
currentProfileNameRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileNameRaw);
return currentProfileName;
}
Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
@@ -149,69 +165,9 @@ class ClashCore {
});
}
ExternalProvider? getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
if(externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
}
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 data,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
}
Future<String> updateExternalProvider({
required String providerName,
required String providerType,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
@@ -222,11 +178,14 @@ class ClashCore {
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final providerTypeChar = providerType.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
providerTypeChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(providerTypeChar);
return completer.future;
}
@@ -237,14 +196,6 @@ class ClashCore {
malloc.free(paramsChar);
}
start() {
clashFFI.start();
}
stop() {
clashFFI.stop();
}
Future<Delay> getDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
@@ -265,13 +216,21 @@ class ClashCore {
receiver.sendPort.nativePort,
);
malloc.free(delayParamsChar);
Future.delayed(httpTimeoutDuration + moreDuration, () {
receiver.close();
if (!completer.isCompleted) {
completer.complete(
Delay(name: proxyName, value: -1),
);
}
});
return completer.future;
}
clearEffect(String profileId) {
final profileIdChar = profileId.toNativeUtf8().cast<Char>();
clashFFI.clearEffect(profileIdChar);
malloc.free(profileIdChar);
clearEffect(String path) {
final pathChar = path.toNativeUtf8().cast<Char>();
clashFFI.clearEffect(pathChar);
malloc.free(pathChar);
}
VersionInfo getVersionInfo() {

View File

@@ -5144,22 +5144,6 @@ class ClashFFI {
late final __FCmulcr =
__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(
ffi.Pointer<ffi.Char> homeDirStr,
) {
@@ -5206,6 +5190,30 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
void setCurrentProfileName(
ffi.Pointer<ffi.Char> s,
) {
return _setCurrentProfileName(
s,
);
}
late final _setCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setCurrentProfileName');
late final _setCurrentProfileName = _setCurrentProfileNamePtr
.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName();
}
late final _getCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getCurrentProfileName');
late final _getCurrentProfileName =
_getCurrentProfileNamePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void validateConfig(
ffi.Pointer<ffi.Char> s,
int port,
@@ -5401,76 +5409,24 @@ class ClashFFI {
late final _getExternalProviders =
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getExternalProvider(
ffi.Pointer<ffi.Char> name,
) {
return _getExternalProvider(
name,
);
}
late final _getExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Char>)>>('getExternalProvider');
late final _getExternalProvider = _getExternalProviderPtr
.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(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> providerType,
int port,
) {
return _updateExternalProvider(
providerName,
providerType,
port,
);
}
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.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('sideLoadExternalProvider');
late final _sideLoadExternalProvider =
_sideLoadExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
ffi.LongLong)>>('updateExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,

View File

@@ -1,28 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:path/path.dart';
extension ArchiveExt on Archive {
addDirectoryToArchive(String dirPath, String parentPath) {
final dir = Directory(dirPath);
final entities = dir.listSync(recursive: false);
for (final entity in entities) {
final relativePath = relative(entity.path, from: parentPath);
if (entity is File) {
final data = entity.readAsBytesSync();
final archiveFile = ArchiveFile(relativePath, data.length, data);
addFile(archiveFile);
} else if (entity is Directory) {
addDirectoryToArchive(entity.path, parentPath);
}
}
}
add<T>(String name, T raw) {
final data = json.encode(raw);
addFile(
ArchiveFile(name, data.length, data),
);
}
}

View File

@@ -16,13 +16,6 @@ extension ColorExtension on Color {
toLittle() {
return withOpacity(0.03);
}
Color darken([double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(this);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
}
extension ColorSchemeExtension on ColorScheme {
@@ -30,7 +23,6 @@ extension ColorSchemeExtension on ColorScheme {
? copyWith(
surface: Colors.black,
background: Colors.black,
surfaceContainer: surfaceContainer.darken(0.05),
)
: this;
}

View File

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

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:ui';
import 'package:fl_clash/enum/enum.dart';
@@ -16,7 +17,7 @@ const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop ? 40 : 0;
final double kHeaderHeight = system.isDesktop ? (Platform.isMacOS ? 28 : 40) : 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",

View File

@@ -1,8 +1,11 @@
import 'dart:async';
import 'dart:typed_data';
import 'dart:convert';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:path/path.dart';
import 'package:webdav_client/webdav_client.dart';
class DAVClient {
@@ -30,6 +33,8 @@ class DAVClient {
Future<bool> _ping() async {
try {
await client.ping();
await client.mkdir("/$appName");
await client.mkdir("/$appName/$profilesDirectoryName");
return true;
} catch (_) {
return false;
@@ -38,17 +43,65 @@ class DAVClient {
get root => "/$appName";
get backupFile => "$root/backup.zip";
get remoteConfig => "$root/$configKey.json";
backup(Uint8List data) async {
get remoteClashConfig => "$root/$clashConfigKey.json";
get remoteProfiles => "$root/$profilesDirectoryName";
backup() async {
final appController = globalState.appController;
final config = appController.config;
final clashConfig = appController.clashConfig;
await client.mkdir("$root");
await client.write("$backupFile", data);
client.write(
remoteConfig,
utf8.encode(
json.encode(config.toJson()),
),
);
client.write(
remoteClashConfig,
utf8.encode(
json.encode(clashConfig.toJson()),
),
);
await client.remove(remoteProfiles);
for (final profile in config.profiles) {
final path = await appPath.getProfilePath(profile.id);
if (path == null) continue;
await client.writeFromFile(
path,
"$remoteProfiles/${basename(path)}",
);
}
return true;
}
Future<List<int>> recovery() async {
await client.mkdir("$root");
final data = await client.read(backupFile);
return data;
recovery({required RecoveryOption recoveryOption}) async {
final profiles = await client.readDir(remoteProfiles);
final profilesPath = await appPath.getProfilesPath();
for (final file in profiles) {
await client.read2File(
"$remoteProfiles/${file.name}",
join(
profilesPath,
file.name,
),
);
}
final configRaw = utf8.decode((await client.read(remoteConfig)));
final clashConfigRaw = utf8.decode(await client.read(remoteClashConfig));
final config = Config.fromJson(json.decode(configRaw));
final clashConfig = ClashConfig.fromJson(json.decode(clashConfigRaw));
if(recoveryOption == RecoveryOption.onlyProfiles){
globalState.appController.config.update(config, RecoveryOption.onlyProfiles);
}else{
globalState.appController.config.update(config, RecoveryOption.all);
globalState.appController.clashConfig.update(clashConfig);
}
await globalState.appController.applyProfile();
globalState.appController.savePreferences();
return true;
}
}

View File

@@ -1,11 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/models/models.dart' hide Process;
import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart';
import 'system.dart';
import 'windows.dart';
class AutoLaunch {
static AutoLaunch? _instance;
@@ -26,77 +24,18 @@ class AutoLaunch {
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 {
if (Platform.isWindows) {
await windowsDisable();
}
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 {
return await launchAtStartup.disable();
}
updateStatus(AutoLaunchState state) async {
final isOpenTun = state.isOpenTun;
final isAutoLaunch = state.isAutoLaunch;
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) {
updateStatus(bool value) async {
final isEnable = await this.isEnable;
if (isEnable == value) return;
if (value == true) {
enable();
} else {
disable();

View File

@@ -44,7 +44,7 @@ class Navigation {
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
const NavigationItem(
icon: Icon(Icons.storage),
icon: Icon(Icons.swap_vert_circle),
label: "resources",
description: "resourcesDesc",
keep: false,

View File

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

View File

@@ -9,7 +9,6 @@ import 'constant.dart';
class AppPath {
static AppPath? _instance;
Completer<Directory> cacheDir = Completer();
Completer<Directory> downloadDir = Completer();
// Future<Directory> _createDesktopCacheDir() async {
// final path = join(dirname(Platform.resolvedExecutable), 'cache');
@@ -24,9 +23,6 @@ class AppPath {
getApplicationSupportDirectory().then((value) {
cacheDir.complete(value);
});
getDownloadsDirectory().then((value) {
downloadDir.complete(value);
});
// if (Platform.isAndroid) {
// getApplicationSupportDirectory().then((value) {
// cacheDir.complete(value);
@@ -43,11 +39,6 @@ class AppPath {
return _instance!;
}
Future<String> getDownloadDirPath() async {
final directory = await downloadDir.future;
return directory.path;
}
Future<String> getHomeDirPath() async {
final directory = await cacheDir.future;
return directory.path;

View File

@@ -1,31 +1,22 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart';
class Picker {
Future<PlatformFile?> pickerFile() async {
Future<PlatformFile?> pickerConfigFile() async {
final filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
allowMultiple: false,
initialDirectory: await appPath.getDownloadDirPath(),
);
return filePickerResult?.files.first;
}
Future<String?> saveFile(String fileName, Uint8List bytes) async {
final path = await FilePicker.platform.saveFile(
fileName: fileName,
initialDirectory: await appPath.getDownloadDirPath(),
bytes: Platform.isAndroid ? bytes : null,
Future<PlatformFile?> pickerGeoDataFile() async {
final filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
allowMultiple: false,
);
if (!Platform.isAndroid && path != null) {
final file = await File(path).create(recursive: true);
await file.writeAsBytes(bytes);
}
return path;
return filePickerResult?.files.first;
}
Future<String?> pickerConfigQRCode() async {

View File

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

110
lib/common/service.dart Normal file
View File

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

View File

@@ -18,12 +18,6 @@ class System {
bool get isDesktop =>
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 {
await app?.moveTaskToBack();
await window?.hide();

View File

@@ -20,7 +20,6 @@ class Window {
WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height),
minimumSize: const Size(380, 500),
windowButtonVisibility: false,
titleBarStyle: TitleBarStyle.hidden,
);
if (props.left != null || props.top != null) {
@@ -34,7 +33,7 @@ class Window {
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
// }
await windowManager.waitUntilReadyToShow(windowOptions, () async {
// await windowManager.setPreventClose(true);
await windowManager.setPreventClose(true);
});
}

View File

@@ -1,59 +0,0 @@
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

@@ -1,15 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:archive/archive.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -26,7 +20,6 @@ class AppController {
late Function updateClashConfigDebounce;
late Function updateGroupDebounce;
late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
AppController(this.context) {
appState = context.read<AppState>();
@@ -35,9 +28,6 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
});
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++;
});
@@ -47,9 +37,10 @@ class AppController {
measure = Measure.of(context);
}
updateStatus(bool isStart) async {
Future<void> updateSystemProxy(bool isStart) async {
if (isStart) {
await globalState.handleStart(
await globalState.startSystemProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
@@ -59,9 +50,10 @@ class AppController {
updateRunTime,
updateTraffic,
];
applyProfileDebounce();
if (Platform.isAndroid) return;
await applyProfile(isPrue: true);
} else {
await globalState.handleStop();
await globalState.stopSystemProxy();
clashCore.resetTraffic();
appState.traffics = [];
appState.totalTraffic = Traffic();
@@ -75,9 +67,8 @@ class AppController {
}
updateRunTime() {
final startTime = globalState.startTime;
if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch;
if (proxyManager.startTime != null) {
final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appState.runTime = nowTimeStamp - startTimeStamp;
} else {
@@ -99,22 +90,22 @@ class AppController {
deleteProfile(String id) async {
config.deleteProfileById(id);
clashCore.clearEffect(id);
final profilePath = await appPath.getProfilePath(id);
if (profilePath == null) return;
clashCore.clearEffect(profilePath);
if (config.currentProfileId == id) {
if (config.profiles.isNotEmpty) {
final updateId = config.profiles.first.id;
changeProfile(updateId);
} else {
updateStatus(false);
updateSystemProxy(false);
}
}
}
Future<void> updateProfile(Profile profile) async {
final newProfile = await profile.update();
config.setProfile(
newProfile.copyWith(isUpdating: false),
);
await profile.update();
config.setProfile(await profile.update());
}
Future<void> updateClashConfig({bool isPatch = true}) async {
@@ -149,6 +140,9 @@ class AppController {
changeProfile(String? value) async {
if (value == config.currentProfileId) return;
config.currentProfileId = value;
await applyProfile();
appState.delayMap = {};
saveConfigPreferences();
}
autoUpdateProfiles() async {
@@ -231,7 +225,7 @@ class AppController {
}
handleExit() async {
await updateStatus(false);
await updateSystemProxy(false);
await savePreferences();
clashCore.shutdown();
system.exit();
@@ -300,13 +294,31 @@ class AppController {
if (!config.silentLaunch) {
window?.show();
}
if (Platform.isAndroid) {
globalState.updateStartTime();
}
if (globalState.isStart) {
await updateStatus(true);
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted == true) {
await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}, title: appLocalizations.init);
} else {
await updateStatus(config.autoRun);
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
await afterInit();
}
afterInit() async {
await proxyManager.updateStartTime();
if (proxyManager.isStart) {
await updateSystemProxy(true);
} else {
await updateSystemProxy(config.autoRun);
}
autoUpdateProfiles();
autoCheckUpdate();
@@ -370,10 +382,6 @@ class AppController {
);
}
showSnackBar(String message) {
globalState.showSnackBar(context, message: message);
}
addProfileFormURL(String url) async {
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
@@ -395,7 +403,7 @@ class AppController {
}
addProfileFormFile() async {
final platformFile = await globalState.safeRun(picker.pickerFile);
final platformFile = await globalState.safeRun(picker.pickerConfigFile);
final bytes = platformFile?.bytes;
if (bytes == null) {
return null;
@@ -423,6 +431,8 @@ class AppController {
addProfileFormURL(url);
}
int get columns => other.getColumns(appState.viewMode, config.proxiesColumns);
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;
@@ -432,10 +442,7 @@ class AppController {
List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
(a, b) => other.sortByChar(
PinyinHelper.getPinyin(a.name),
PinyinHelper.getPinyin(b.name),
),
(a, b) => other.sortByChar(a.name, b.name),
);
}
@@ -473,63 +480,4 @@ class AppController {
config.currentSelectedMap[groupName] ?? '') ??
'';
}
Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath();
final profilesPath = await appPath.getProfilesPath();
final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async {
final archive = Archive();
archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? [];
});
}
recoveryData(
List<int> data,
RecoveryOption recoveryOption,
) async {
final archive = await Isolate.run<Archive>(() {
final zipDecoder = ZipDecoder();
return zipDecoder.decodeBytes(data);
});
final homeDirPath = await appPath.getHomeDirPath();
final configs =
archive.files.where((item) => item.name.endsWith(".json")).toList();
final profiles =
archive.files.where((item) => !item.name.endsWith(".json"));
final configIndex =
configs.indexWhere((config) => config.name == "config.json");
final clashConfigIndex =
configs.indexWhere((config) => config.name == "clashConfig.json");
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
final configFile = configs[configIndex];
final clashConfigFile = configs[clashConfigIndex];
final tempConfig = Config.fromJson(
json.decode(
utf8.decode(configFile.content),
),
);
final tempClashConfig = ClashConfig.fromJson(
json.decode(
utf8.decode(clashConfigFile.content),
),
);
for (final profile in profiles) {
final filePath = join(homeDirPath, profile.name);
final file = File(filePath);
await file.create(recursive: true);
await file.writeAsBytes(profile.content);
}
if (recoveryOption == RecoveryOption.onlyProfiles) {
config.update(tempConfig, RecoveryOption.onlyProfiles);
} else {
config.update(tempConfig, RecoveryOption.all);
clashConfig.update(tempClashConfig);
}
}
}

View File

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

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
@@ -7,9 +6,15 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
extension AccessControlExtension on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
class AccessFragment extends StatefulWidget {
const AccessFragment({super.key});
@@ -18,100 +23,319 @@ class AccessFragment extends StatefulWidget {
}
class _AccessFragmentState extends State<AccessFragment> {
List<String> acceptList = [];
List<String> rejectList = [];
final packagesListenable = ValueNotifier<List<Package>>([]);
@override
void initState() {
super.initState();
_updateInitList();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
if (appState.packages.isEmpty) {
Future.delayed(const Duration(milliseconds: 300), () async {
appState.packages = await app?.getPackages() ?? [];
});
}
Future.delayed(const Duration(milliseconds: 300), () async {
packagesListenable.value = await app?.getPackages() ?? [];
});
});
}
_updateInitList() {
final accessControl = globalState.appController.config.accessControl;
acceptList = accessControl.acceptList;
rejectList = accessControl.rejectList;
@override
void dispose() {
super.dispose();
packagesListenable.dispose();
}
Widget _buildSearchButton() {
Widget _buildAppProxyModePopup() {
final items = [
CommonPopupMenuItem(
action: AccessControlMode.rejectSelected,
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() {
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(
tooltip: appLocalizations.search,
onPressed: () {
showSearch(
context: context,
delegate: AccessControlSearchDelegate(
acceptList: acceptList,
rejectList: rejectList,
packages: packages,
),
).then((_) => setState(() {
_updateInitList();
}));
).then((_) => {setState(() {})});
},
icon: const Icon(Icons.search),
);
}
Widget _buildSelectedAllButton({
required bool isSelectedAll,
required List<String> allValueList,
}) {
final tooltip = isSelectedAll
? appLocalizations.cancelSelectAll
: appLocalizations.selectAll;
return IconButton(
tooltip: tooltip,
onPressed: () {
final config = globalState.appController.config;
final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: [],
),
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
icon: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
);
}
// Widget _buildSelectedAllButton({
// required bool isAccessControl,
// required bool isSelectedAll,
// required List<String> allValueList,
// }) {
// final tooltip = isSelectedAll
// ? appLocalizations.cancelSelectAll
// : appLocalizations.selectAll;
// return AbsorbPointer(
// absorbing: !isAccessControl,
// child: FloatingActionButton(
// tooltip: tooltip,
// onPressed: () {
// final config = globalState.appController.config;
// final isAccept =
// config.accessControl.mode == AccessControlMode.acceptSelected;
//
// if (isSelectedAll) {
// config.accessControl = switch (isAccept) {
// true => config.accessControl.copyWith(
// acceptList: [],
// ),
// false => config.accessControl.copyWith(
// rejectList: [],
// ),
// };
// } else {
// config.accessControl = switch (isAccept) {
// true => config.accessControl.copyWith(
// acceptList: allValueList,
// ),
// false => config.accessControl.copyWith(
// rejectList: allValueList,
// ),
// };
// }
// },
// child: 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,
Widget _buildPackageList() {
return ValueListenableBuilder(
valueListenable: packagesListenable,
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: FadeBox(
key: const Key("fade_box"),
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,
);
}
},
);
},
),
),
),
],
),
);
},
);
},
icon: const Icon(Icons.tune),
);
}
@@ -148,170 +372,7 @@ class _AccessFragmentState extends State<AccessFragment> {
],
);
},
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,
);
}
},
);
},
),
),
],
),
);
},
);
},
),
child: _buildPackageList(),
);
}
}
@@ -378,14 +439,23 @@ class PackageListItem extends StatelessWidget {
}
class AccessControlSearchDelegate extends SearchDelegate {
List<String> acceptList = [];
List<String> rejectList = [];
final List<Package> packages;
AccessControlSearchDelegate({
required this.acceptList,
required this.rejectList,
required this.packages,
});
List<Package> get _results {
final lowQuery = query.toLowerCase();
return packages
.where(
(package) =>
package.label.toLowerCase().contains(lowQuery) ||
package.packageName.contains(lowQuery),
)
.toList();
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
@@ -415,39 +485,26 @@ class AccessControlSearchDelegate extends SearchDelegate {
);
}
Widget _packageList() {
final lowQuery = query.toLowerCase();
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
packages: appState.packages,
Widget _packageList(List<Package> packages) {
return Selector<Config, PackageListSelectorState>(
selector: (_, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
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 accessControlMode = accessControl.mode;
final currentList = accessControl.currentList;
final packageNameList = packages.map((e) => e.packageName).toList();
final packageNameList =
this.packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
return DisabledMask(
status: !isAccessControl,
child: ListView.builder(
itemCount: queryPackages.length,
itemCount: packages.length,
itemBuilder: (_, index) {
final package = queryPackages[index];
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
@@ -485,268 +542,6 @@ class AccessControlSearchDelegate extends SearchDelegate {
@override
Widget buildSuggestions(BuildContext context) {
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(),
],
),
);
return _packageList(_results);
}
}

View File

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

View File

@@ -1,5 +1,3 @@
import 'dart:typed_data';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/dav_client.dart';
import 'package:fl_clash/enum/enum.dart';
@@ -12,9 +10,16 @@ import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class BackupAndRecovery extends StatelessWidget {
class BackupAndRecovery extends StatefulWidget {
const BackupAndRecovery({super.key});
@override
State<BackupAndRecovery> createState() => _BackupAndRecoveryState();
}
class _BackupAndRecoveryState extends State<BackupAndRecovery> {
DAVClient? _client;
_showAddWebDAV(DAV? dav) async {
await globalState.showCommonDialog<String>(
child: WebDAVFormDialog(
@@ -23,15 +28,11 @@ class BackupAndRecovery extends StatelessWidget {
);
}
_backupOnWebDAV(BuildContext context, DAVClient client) async {
_backup() async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
final backupData = await globalState.appController.backupData();
return await client.backup(Uint8List.fromList(backupData));
},
title: appLocalizations.backup,
);
final res = await commonScaffoldState?.loadingRun<bool>(() async {
return await _client?.backup();
});
if (res != true) return;
globalState.showMessage(
title: appLocalizations.backup,
@@ -39,20 +40,11 @@ class BackupAndRecovery extends StatelessWidget {
);
}
_recoveryOnWebDAV(
BuildContext context,
DAVClient client,
RecoveryOption recoveryOption,
) async {
_recovery(RecoveryOption recoveryOption) async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
final data = await client.recovery();
await globalState.appController.recoveryData(data, recoveryOption);
return true;
},
title: appLocalizations.recovery,
);
final res = await commonScaffoldState?.loadingRun<bool>(() async {
return await _client?.recovery(recoveryOption: recoveryOption);
});
if (res != true) return;
globalState.showMessage(
title: appLocalizations.recovery,
@@ -60,66 +52,12 @@ class BackupAndRecovery extends StatelessWidget {
);
}
_handleRecoveryOnWebDAV(BuildContext context, DAVClient client) async {
_handleRecovery() async {
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
child: const RecoveryOptionsDialog(),
);
if (recoveryOption == null || !context.mounted) return;
_recoveryOnWebDAV(context, client, recoveryOption);
}
_backupOnLocal(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
final backupData = await globalState.appController.backupData();
final value = await picker.saveFile(
other.getBackupFileName(),
Uint8List.fromList(backupData),
);
if(value == null) return false;
return true;
},
title: appLocalizations.backup,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.backup,
message: TextSpan(text: appLocalizations.backupSuccess),
);
}
_recoveryOnLocal(
BuildContext context,
RecoveryOption recoveryOption,
) async {
final file = await picker.pickerFile();
final data = file?.bytes;
if (data == null || !context.mounted) return;
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
await globalState.appController.recoveryData(
List<int>.from(data),
recoveryOption,
);
return true;
},
title: appLocalizations.recovery,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.recovery,
message: TextSpan(text: appLocalizations.recoverySuccess),
);
}
_handleRecoveryOnLocal(BuildContext context) async {
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
child: const RecoveryOptionsDialog(),
);
if (recoveryOption == null || !context.mounted) return;
_recoveryOnLocal(context, recoveryOption);
if (recoveryOption == null) return;
_recovery(recoveryOption);
}
@override
@@ -127,11 +65,12 @@ class BackupAndRecovery extends StatelessWidget {
return Selector<Config, DAV?>(
selector: (_, config) => config.dav,
builder: (_, dav, __) {
final client = dav != null ? DAVClient(dav) : null;
return ListView(
children: [
ListHeader(title: appLocalizations.remote),
if (dav == null)
if (dav == null) {
return ListView(
children: [
ListHeader(
title: appLocalizations.account,
),
ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
@@ -144,95 +83,95 @@ class BackupAndRecovery extends StatelessWidget {
appLocalizations.bind,
),
),
)
else ...[
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeBox(
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
const SizedBox(
height: 4,
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
);
}
_client = DAVClient(dav);
final pingFuture = _client!.pingCompleter.future;
return ListView(
children: [
ListHeader(title: appLocalizations.account),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: pingFuture,
builder: (_, snapshot) {
return Center(
child: FadeBox(
key: const Key("fade_box_1"),
child: snapshot.connectionState == ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
FutureBuilder<bool>(
future: pingFuture,
builder: (_, snapshot) {
return FadeBox(
key: const Key("fade_box_2"),
child: snapshot.data == true
? Column(
children: [
ListHeader(
title: appLocalizations.backupAndRecovery),
ListItem(
onTap: _backup,
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.backupDesc),
),
ListItem(
onTap: _handleRecovery,
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.recoveryDesc),
),
],
)
: Container(),
);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
);
@@ -241,50 +180,6 @@ class BackupAndRecovery extends StatelessWidget {
}
}
class RecoveryOptionsDialog extends StatefulWidget {
const RecoveryOptionsDialog({super.key});
@override
State<RecoveryOptionsDialog> createState() => _RecoveryOptionsDialogState();
}
class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
_handleOnTab(RecoveryOption? value) {
if (value == null) return;
Navigator.of(context).pop(value);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.recovery),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),
)
],
),
),
);
}
}
class WebDAVFormDialog extends StatefulWidget {
final DAV? dav;
@@ -343,7 +238,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
children: [
TextFormField(
controller: uriController,
maxLines: 5,
maxLines: 2,
minLines: 1,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link),
@@ -418,3 +313,47 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
);
}
}
class RecoveryOptionsDialog extends StatefulWidget {
const RecoveryOptionsDialog({super.key});
@override
State<RecoveryOptionsDialog> createState() => _RecoveryOptionsDialogState();
}
class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
_handleOnTab(RecoveryOption? value) {
if (value == null) return;
Navigator.of(context).pop(value);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.recovery),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),
)
],
),
),
);
}
}

View File

@@ -27,6 +27,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
final mixedPort = int.parse(port);
if (mixedPort < 1024 || mixedPort > 49151) throw "Invalid port";
globalState.appController.clashConfig.mixedPort = mixedPort;
globalState.appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: appLocalizations.proxyPort,
@@ -61,6 +62,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
}
final appController = globalState.appController;
appController.clashConfig.logLevel = value;
appController.updateClashConfigDebounce();
Navigator.of(context).pop();
},
),
@@ -98,6 +100,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (String? value) {
final appController = globalState.appController;
appController.clashConfig.globalRealUa = value;
appController.updateClashConfigDebounce();
Navigator.of(context).pop();
},
),
@@ -122,34 +125,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
throw "Invalid url";
}
globalState.appController.config.testUrl = newTestUrl;
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
}
}
}
_updateKeepAliveInterval(int keepAliveInterval) async {
final newKeepAliveIntervalString =
await globalState.showCommonDialog<String>(
child: KeepAliveIntervalFormDialog(
keepAliveInterval: keepAliveInterval,
),
);
if (newKeepAliveIntervalString != null &&
newKeepAliveIntervalString != "$keepAliveInterval" &&
mounted) {
try {
final newKeepAliveInterval = int.parse(newKeepAliveIntervalString);
if (newKeepAliveInterval <= 0) {
throw "Invalid keepAliveInterval";
}
globalState.appController.clashConfig.keepAliveInterval =
newKeepAliveInterval;
globalState.appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
@@ -166,9 +141,9 @@ class _ConfigFragmentState extends State<ConfigFragment> {
return generateSection(
title: appLocalizations.app,
items: [
if (Platform.isAndroid) ...[
if (Platform.isAndroid)...[
Selector<Config, bool>(
selector: (_, config) => config.vpnProps.allowBypass,
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined),
@@ -177,18 +152,15 @@ class _ConfigFragmentState extends State<ConfigFragment> {
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
allowBypass: value,
);
final appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.vpnProps.systemProxy,
selector: (_, config) => config.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet),
@@ -197,11 +169,8 @@ class _ConfigFragmentState extends State<ConfigFragment> {
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
systemProxy: value,
);
final appController = globalState.appController;
appController.config.systemProxy = value;
},
),
);
@@ -294,19 +263,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
Selector<ClashConfig, int>(
selector: (_, config) => config.keepAliveInterval,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.timer_outlined),
title: Text(appLocalizations.keepAliveIntervalDesc),
subtitle: Text("$value ${appLocalizations.seconds}"),
onTap: () {
_updateKeepAliveInterval(value);
},
);
},
),
Selector<Config, String>(
selector: (_, config) => config.testUrl,
builder: (_, value, __) {
@@ -353,6 +309,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
appController.updateClashConfigDebounce();
},
),
);
@@ -370,6 +327,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.allowLan = value;
globalState.appController.updateClashConfigDebounce();
},
),
);
@@ -387,6 +345,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value;
appController.updateClashConfigDebounce();
},
),
);
@@ -406,6 +365,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
appController.updateClashConfigDebounce();
},
),
);
@@ -423,6 +383,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
appController.updateClashConfigDebounce();
},
),
);
@@ -443,6 +404,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative
: geodataLoaderStandard;
appController.updateClashConfigDebounce();
},
),
);
@@ -462,6 +424,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
appController.updateClashConfigDebounce();
},
),
);
@@ -488,6 +451,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value);
globalState.appController.updateClashConfigDebounce();
},
),
);
@@ -625,65 +589,3 @@ class _TestUrlFormDialogState extends State<TestUrlFormDialog> {
);
}
}
class KeepAliveIntervalFormDialog extends StatefulWidget {
final int keepAliveInterval;
const KeepAliveIntervalFormDialog({
super.key,
required this.keepAliveInterval,
});
@override
State<KeepAliveIntervalFormDialog> createState() =>
_KeepAliveIntervalFormDialogState();
}
class _KeepAliveIntervalFormDialogState
extends State<KeepAliveIntervalFormDialog> {
late TextEditingController keepAliveIntervalController;
@override
void initState() {
super.initState();
keepAliveIntervalController = TextEditingController(
text: "${widget.keepAliveInterval}",
);
}
_handleUpdate() async {
final keepAliveInterval = keepAliveIntervalController.value.text;
if (keepAliveInterval.isEmpty) return;
Navigator.of(context).pop<String>(keepAliveInterval);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.keepAliveIntervalDesc),
content: SizedBox(
width: 300,
child: Wrap(
runSpacing: 16,
children: [
TextField(
maxLines: 1,
minLines: 1,
controller: keepAliveIntervalController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
suffixText: appLocalizations.seconds,
),
),
],
),
),
actions: [
TextButton(
onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
],
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,121 +0,0 @@
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

@@ -121,7 +121,7 @@ class _EditProfileState extends State<EditProfile> {
}
_uploadProfileFile() async {
final platformFile = await globalState.safeRun(picker.pickerFile);
final platformFile = await globalState.safeRun(picker.pickerConfigFile);
if (platformFile?.bytes == null) return;
fileData = platformFile?.bytes;
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
@@ -260,19 +260,23 @@ class _EditProfileState extends State<EditProfile> {
padding: const EdgeInsets.symmetric(
vertical: 16,
),
child: ListView.separated(
padding: kMaterialListPadding.copyWith(
bottom: 72,
),
itemBuilder: (_, index) {
return items[index];
},
separatorBuilder: (_, __) {
return const SizedBox(
height: 24,
child: ScrollOverBuilder(
builder: (isOver) {
return ListView.separated(
padding: kMaterialListPadding.copyWith(
bottom: isOver ? 72 : 36,
),
itemBuilder: (_, index) {
return items[index];
},
separatorBuilder: (_, __) {
return const SizedBox(
height: 24,
);
},
itemCount: items.length,
);
},
itemCount: items.length,
),
),
),

View File

@@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/models/models.dart';
@@ -29,6 +27,8 @@ class ProfilesFragment extends StatefulWidget {
class _ProfilesFragmentState extends State<ProfilesFragment> {
Function? applyConfigDebounce;
List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = [];
_handleShowAddExtendPage() {
showExtendPage(
globalState.navigatorKey.currentState!.context,
@@ -39,52 +39,29 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
}
_updateProfiles() async {
final appController = globalState.appController;
final config = appController.config;
final profiles = appController.config.profiles;
final messages = [];
final updateProfiles = profiles.map<Future>(
(profile) async {
config.setProfile(
profile.copyWith(isUpdating: true),
);
try {
await appController.updateProfile(profile);
if (profile.id == appController.config.currentProfile?.id) {
appController.applyProfileDebounce();
}
} catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile(
profile.copyWith(
isUpdating: false,
),
);
}
},
);
final titleMedium = context.textTheme.titleMedium;
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)
],
),
);
_getColumns(ViewMode viewMode) {
switch (viewMode) {
case ViewMode.mobile:
return 1;
case ViewMode.laptop:
return 1;
case ViewMode.desktop:
return 2;
}
}
_updateProfiles() async {
final updateProfiles = profileItemKeys.map<Future>(
(key) async => await key.currentState?.updateProfile(false));
await Future.wait(updateProfiles);
}
_initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (!mounted) return;
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
if (!context.mounted) return;
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
@@ -92,29 +69,24 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
},
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,
),
];
},
);
}
_changeProfile(String? id) async {
final appController = globalState.appController;
final config = appController.config;
if (id == config.currentProfileId) return;
config.currentProfileId = id;
applyConfigDebounce ??= debounce<Function()>(() async {
await appController.applyProfile();
appController.appState.delayMap = {};
appController.saveConfigPreferences();
});
applyConfigDebounce!();
}
@override
Widget build(BuildContext context) {
return FloatLayout(
@@ -139,7 +111,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
columns: other.getProfilesColumns(appState.viewWidth),
viewMode: appState.viewMode,
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
@@ -147,31 +119,40 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
label: appLocalizations.nullProfileDesc,
);
}
profileItemKeys = state.profiles
.map(
(profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
.toList();
final columns = _getColumns(state.viewMode);
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
child: ScrollOverBuilder(
builder: (isOver) {
return SingleChildScrollView(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + (isOver ? 72 : 0),
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: profileItemKeys[i],
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: _changeProfile,
),
),
],
),
);
},
),
);
},
@@ -181,7 +162,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
class ProfileItem extends StatelessWidget {
class ProfileItem extends StatefulWidget {
final Profile profile;
final String? groupValue;
final void Function(String? value) onChanged;
@@ -193,15 +174,22 @@ class ProfileItem extends StatelessWidget {
required this.onChanged,
});
_handleDeleteProfile(BuildContext context) async {
@override
State<ProfileItem> createState() => _ProfileItemState();
}
class _ProfileItemState extends State<ProfileItem> {
final isUpdating = ValueNotifier<bool>(false);
_handleDeleteProfile() async {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: appLocalizations.deleteProfileTip,
),
onTab: () async {
await globalState.appController.deleteProfile(profile.id);
if (context.mounted) {
await globalState.appController.deleteProfile(widget.profile.id);
if (mounted) {
Navigator.of(context).pop();
}
},
@@ -212,47 +200,41 @@ class ProfileItem extends StatelessWidget {
await globalState.safeRun<void>(updateProfile);
}
Future updateProfile() async {
final appController = globalState.appController;
final config = appController.config;
if (profile.type == ProfileType.file) return;
await globalState.safeRun(() async {
try {
config.setProfile(
profile.copyWith(
isUpdating: true,
),
);
await appController.updateProfile(profile);
if (profile.id == appController.config.currentProfile?.id) {
appController.applyProfileDebounce();
}
} catch (e) {
config.setProfile(
profile.copyWith(
isUpdating: false,
),
);
Future updateProfile([isSingle = true]) async {
isUpdating.value = true;
try {
final appController = globalState.appController;
await appController.updateProfile(widget.profile);
if (widget.profile.id == appController.config.currentProfile?.id) {
globalState.appController.applyProfile(isPrue: true);
}
} catch (e) {
isUpdating.value = false;
if (!isSingle) {
return e.toString();
} else {
rethrow;
}
});
}
isUpdating.value = false;
return null;
}
_handleShowEditExtendPage(BuildContext context) {
_handleShowEditExtendPage() {
showExtendPage(
context,
body: EditProfile(
profile: profile,
profile: widget.profile,
context: context,
),
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
}
List<Widget> _buildUserInfo(BuildContext context, UserInfo userInfo) {
List<Widget> _buildUserInfo(UserInfo userInfo) {
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
if (total == 0) {
if(total == 0){
return [];
}
final useShow = TrafficValue(value: use).show;
@@ -265,7 +247,6 @@ class ProfileItem extends StatelessWidget {
LinearProgressIndicator(
minHeight: 6,
value: progress,
backgroundColor: context.colorScheme.primary.toSoft(),
),
const SizedBox(
height: 8,
@@ -280,13 +261,13 @@ class ProfileItem extends StatelessWidget {
];
}
List<Widget> _buildUrlProfileInfo(BuildContext context) {
List<Widget> _buildUrlProfileInfo(Profile profile) {
final userInfo = profile.userInfo;
return [
const SizedBox(
height: 8,
),
if (userInfo != null) ..._buildUserInfo(context, userInfo),
if (userInfo != null) ..._buildUserInfo(userInfo),
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? "",
style: context.textTheme.labelMedium?.toLight,
@@ -294,7 +275,7 @@ class ProfileItem extends StatelessWidget {
];
}
List<Widget> _buildFileProfileInfo(BuildContext context) {
List<Widget> _buildFileProfileInfo(Profile profile) {
return [
const SizedBox(
height: 8,
@@ -306,8 +287,50 @@ class ProfileItem extends StatelessWidget {
];
}
_buildTitle(Profile profile) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
profile.label ?? profile.id,
style: context.textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
...switch (profile.type) {
ProfileType.file => _buildFileProfileInfo(
profile,
),
ProfileType.url => _buildUrlProfileInfo(
profile,
),
},
],
),
],
),
);
}
@override
void dispose() {
isUpdating.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final profile = widget.profile;
final groupValue = widget.groupValue;
final onChanged = widget.onChanged;
return CommonCard(
isSelected: profile.id == groupValue,
onPressed: () {
@@ -320,203 +343,57 @@ class ProfileItem extends StatelessWidget {
trailing: SizedBox(
height: 40,
width: 40,
child: FadeBox(
child: profile.isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: CommonPopupMenu<ProfileActions>(
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,
label: appLocalizations.edit,
iconData: Icons.edit,
child: ValueListenableBuilder(
valueListenable: isUpdating,
builder: (_, isUpdating, ___) {
return FadeBox(
child: isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: CommonPopupMenu<ProfileActions>(
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,
label: appLocalizations.edit,
iconData: Icons.edit,
),
if (profile.type == ProfileType.url)
CommonPopupMenuItem(
action: ProfileActions.update,
label: appLocalizations.update,
iconData: Icons.sync,
),
CommonPopupMenuItem(
action: ProfileActions.delete,
label: appLocalizations.delete,
iconData: Icons.delete,
),
],
onSelected: (ProfileActions? action) async {
switch (action) {
case ProfileActions.edit:
_handleShowEditExtendPage();
break;
case ProfileActions.delete:
_handleDeleteProfile();
break;
case ProfileActions.update:
_handleUpdateProfile();
break;
case null:
break;
}
},
),
if (profile.type == ProfileType.url)
CommonPopupMenuItem(
action: ProfileActions.update,
label: appLocalizations.update,
iconData: Icons.sync,
),
CommonPopupMenuItem(
action: ProfileActions.delete,
label: appLocalizations.delete,
iconData: Icons.delete,
),
],
onSelected: (ProfileActions? action) async {
switch (action) {
case ProfileActions.edit:
_handleShowEditExtendPage(context);
break;
case ProfileActions.delete:
_handleDeleteProfile(context);
break;
case ProfileActions.update:
_handleUpdateProfile();
break;
case null:
break;
}
},
),
),
),
title: Container(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
profile.label ?? profile.id,
style: context.textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
...switch (profile.type) {
ProfileType.file => _buildFileProfileInfo(context),
ProfileType.url => _buildUrlProfileInfo(context),
},
],
),
],
);
},
),
),
title: _buildTitle(profile),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
),
);
}
}
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

@@ -1,232 +0,0 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:re_editor/re_editor.dart';
import 'package:re_highlight/languages/yaml.dart';
import 'package:re_highlight/styles/atom-one-light.dart';
class ViewProfile extends StatefulWidget {
final Profile profile;
const ViewProfile({
super.key,
required this.profile,
});
@override
State<ViewProfile> createState() => _ViewProfileState();
}
class _ViewProfileState extends State<ViewProfile> {
bool readOnly = true;
final CodeLineEditingController _controller = CodeLineEditingController();
final key = GlobalKey<CommonScaffoldState>();
final _focusNode = FocusNode();
String? rawText;
@override
void initState() {
super.initState();
appPath.getProfilePath(widget.profile.id).then((path) async {
if (path == null) return;
final file = File(path);
rawText = await file.readAsString();
_controller.text = rawText ?? "";
});
}
@override
void dispose() {
super.dispose();
_controller.dispose();
_focusNode.dispose();
}
Profile get profile => widget.profile;
_handleChangeReadOnly() async {
if (readOnly == true) {
setState(() {
readOnly = false;
});
} else {
if (_controller.text == rawText) return;
final newProfile = await key.currentState?.loadingRun<Profile>(() async {
return await profile.saveFileWithString(_controller.text);
});
if (newProfile == null) return;
globalState.appController.config.setProfile(newProfile);
setState(() {
readOnly = true;
});
}
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
key: key,
actions: [
IconButton(
onPressed: _controller.undo,
icon: const Icon(Icons.undo),
),
IconButton(
onPressed: _controller.redo,
icon: const Icon(Icons.redo),
),
IconButton(
onPressed: _handleChangeReadOnly,
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
),
],
body: CodeEditor(
readOnly: readOnly,
focusNode: _focusNode,
scrollbarBuilder: (context, child, details) {
return Scrollbar(
controller: details.controller,
thickness: 8,
radius: const Radius.circular(2),
interactive: true,
child: child,
);
},
showCursorWhenReadOnly: false,
controller: _controller,
shortcutsActivatorsBuilder:
const DefaultCodeShortcutsActivatorsBuilder(),
indicatorBuilder: (
context,
editingController,
chunkController,
notifier,
) {
return Row(
children: [
DefaultCodeLineNumber(
controller: editingController,
notifier: notifier,
),
DefaultCodeChunkIndicator(
width: 20,
controller: chunkController,
notifier: notifier,
)
],
);
},
toolbarController:
!readOnly ? ContextMenuControllerImpl(_focusNode) : null,
style: CodeEditorStyle(
fontSize: 14,
codeTheme: CodeHighlightTheme(
languages: {
'yaml': CodeHighlightThemeMode(
mode: langYaml,
)
},
theme: atomOneLightTheme,
),
),
),
title: widget.profile.label ?? widget.profile.id,
);
}
}
class ContextMenuItemWidget extends PopupMenuItem<void> {
ContextMenuItemWidget({
super.key,
required String text,
required VoidCallback super.onTap,
}) : super(child: Text(text));
}
class ContextMenuControllerImpl implements SelectionToolbarController {
OverlayEntry? _overlayEntry;
final FocusNode focusNode;
ContextMenuControllerImpl(
this.focusNode,
);
_removeOverLayEntry() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override
void hide(BuildContext context) {
// _removeOverLayEntry();
}
_handleCut(CodeLineEditingController controller) {
controller.cut();
_removeOverLayEntry();
}
_handleCopy(CodeLineEditingController controller) async {
await controller.copy();
_removeOverLayEntry();
}
_handlePaste(CodeLineEditingController controller) {
controller.paste();
_removeOverLayEntry();
}
@override
void show({
required BuildContext context,
required CodeLineEditingController controller,
required TextSelectionToolbarAnchors anchors,
Rect? renderRect,
required LayerLink layerLink,
required ValueNotifier<bool> visibility,
}) {
if (controller.selectedText.isEmpty) {
return;
}
_removeOverLayEntry();
final relativeRect = RelativeRect.fromSize(
(anchors.primaryAnchor) &
const Size(150, double.infinity),
MediaQuery.of(context).size,
);
_overlayEntry ??= OverlayEntry(
builder: (context) => ValueListenableBuilder<CodeLineEditingValue>(
valueListenable: controller,
builder: (_, __, child) {
if (controller.selectedText.isEmpty) {
_removeOverLayEntry();
}
return child!;
},
child: Positioned(
left: relativeRect.left,
top: relativeRect.top,
child: Material(
color: Colors.transparent,
child: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(focusNode);
},
child: Container(
width: 200,
height: 200,
color: Colors.green,
),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
}

View File

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

View File

@@ -1,7 +1,7 @@
import 'dart:math';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/other.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -42,7 +42,7 @@ double getItemHeight(ProxyCardType proxyCardType) {
delayTest(List<Proxy> proxies) async {
final appController = globalState.appController;
final delayProxies = proxies.map<Future>((proxy) async {
for (final proxy in proxies) {
final proxyName = appController.appState.getRealProxyName(proxy.name);
globalState.appController.setDelay(
Delay(
@@ -50,9 +50,11 @@ delayTest(List<Proxy> proxies) async {
value: 0,
),
);
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
});
await Future.wait(delayProxies);
clashCore.getDelay(proxyName).then((delay) {
globalState.appController.setDelay(delay);
});
}
await Future.delayed(httpTimeoutDuration + moreDuration);
appController.appState.sortNum++;
}
@@ -61,10 +63,7 @@ double getScrollToSelectedOffset({
required List<Proxy> proxies,
}) {
final appController = globalState.appController;
final columns = other.getProxiesColumns(
appController.appState.viewWidth,
appController.config.proxiesLayout,
);
final columns = appController.columns;
final proxyCardType = appController.config.proxyCardType;
final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere(

View File

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

View File

@@ -1,208 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart';
import 'package:fl_clash/models/ffi.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
typedef UpdatingMap = Map<String, bool>;
class Providers extends StatefulWidget {
const Providers({
super.key,
});
@override
State<Providers> createState() => _ProvidersState();
}
class _ProvidersState extends State<Providers> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
_updateProviders();
},
icon: const Icon(
Icons.sync,
),
)
];
},
);
}
_updateProviders() async {
final appState = globalState.appController.appState;
final providers = globalState.appController.appState.providers;
final updateProviders = providers.map<Future>(
(provider) async {
appState.setProvider(
provider.copyWith(isUpdating: true),
);
await clashCore.updateExternalProvider(
providerName: provider.name,
);
appState.setProvider(
clashCore.getExternalProvider(provider.name),
);
},
);
await Future.wait(updateProviders);
await globalState.appController.updateGroupDebounce();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, List<ExternalProvider>>(
selector: (_, appState) => appState.providers,
builder: (_, providers, ___) {
return ListView.separated(
itemBuilder: (_, index) {
return ProviderItem(
provider: providers[index],
);
},
separatorBuilder: (_, index) {
return const Divider(
height: 0,
);
},
itemCount: providers.length,
);
},
);
}
}
class ProviderItem extends StatelessWidget {
final ExternalProvider provider;
const ProviderItem({
super.key,
required this.provider,
});
_handleUpdateProvider() async {
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();
}
_handleSideLoadProvider() async {
await globalState.safeRun<void>(() async {
final platformFile = await picker.pickerFile();
final appState = globalState.appController.appState;
final bytes = platformFile?.bytes;
if (bytes == null) return;
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),
);
if (message.isNotEmpty) throw message;
appState.setProvider(
clashCore.getExternalProvider(provider.name),
);
if (message.isNotEmpty) throw message;
});
await globalState.appController.updateGroupDebounce();
}
String _buildProviderDesc() {
final baseInfo =
"${provider.type}(${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}";
final count = provider.count;
return switch (count == 0) {
true => baseInfo,
false => "$baseInfo · $count${appLocalizations.entries}",
};
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: Text(provider.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
Text(
_buildProviderDesc(),
),
Text(
provider.path,
style: context.textTheme.bodyMedium?.toLight,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 12,
children: [
CommonChip(
avatar: const Icon(Icons.upload),
label: appLocalizations.upload,
onPressed: _handleSideLoadProvider,
),
if (provider.vehicleType == "HTTP")
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: _handleUpdateProvider,
),
],
),
],
),
trailing: SizedBox(
height: 48,
width: 48,
child: FadeBox(
child: provider.isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: const SizedBox(),
),
),
);
}
}

View File

@@ -6,7 +6,6 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers.dart';
import 'setting.dart';
import 'tab.dart';
@@ -20,37 +19,18 @@ class ProxiesFragment extends StatefulWidget {
class _ProxiesFragmentState extends State<ProxiesFragment> {
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
_initActions(ProxiesType proxiesType, bool hasProvider) {
_initActions(ProxiesType proxiesType) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
if (hasProvider) ...[
IconButton(
onPressed: () {
showExtendPage(
forceNotSide: true,
extendPageWidth: 360,
context,
body: const Providers(),
title: appLocalizations.externalResources,
);
},
icon: const Icon(
Icons.swap_vert_circle_outlined,
),
),
const SizedBox(
width: 8,
),
],
if (proxiesType == ProxiesType.tab) ...[
IconButton(
onPressed: () {
_proxiesTabKey.currentState?.scrollToGroupSelected();
},
icon: const Icon(
Icons.adjust_outlined,
Icons.gps_fixed,
),
),
const SizedBox(
@@ -80,18 +60,18 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
return Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) {
return ProxiesActionsBuilder(
builder: (state, child) {
if (state.isCurrent) {
_initActions(proxiesType, state.hasProvider);
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions(proxiesType);
}
return child!;
},
child: switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment(
key: _proxiesTabKey,
),
ProxiesType.list => const ProxiesListFragment(),
return switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment(
key: _proxiesTabKey,
),
ProxiesType.list => const ProxiesListFragment(),
};
},
);
},

View File

@@ -33,14 +33,6 @@ 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() {
return generateSection(
title: appLocalizations.style,
@@ -140,28 +132,36 @@ class ProxiesSettingWidget extends StatelessWidget {
);
}
List<Widget> _buildLayoutSetting() {
List<Widget> _buildColumnsSetting() {
return generateSection(
title: appLocalizations.layout,
title: appLocalizations.columns,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
scrollDirection: Axis.horizontal,
child: Selector< Config, ProxiesLayout>(
selector: (_, config) => config.proxiesLayout,
builder: (_, proxiesLayout, __) {
child: Selector2<AppState, Config, ColumnsSelectorState>(
selector: (_, appState, config) => ColumnsSelectorState(
columns: config.proxiesColumns,
viewMode: appState.viewMode,
),
builder: (_, state, __) {
final config = globalState.appController.config;
final targetColumnsArray = viewModeColumnsMap[state.viewMode]!;
final currentColumns = other.getColumns(
state.viewMode,
state.columns,
);
return Wrap(
spacing: 16,
children: [
for (final item in ProxiesLayout.values)
for (final item in targetColumnsArray)
SettingTextCard(
getTextForProxiesLayout(item),
isSelected: item == proxiesLayout,
other.getColumnsTextForInt(item),
isSelected: item == currentColumns,
onPressed: () {
config.proxiesLayout = item;
config.proxiesColumns = item;
},
)
],
@@ -183,10 +183,80 @@ class ProxiesSettingWidget extends StatelessWidget {
children: [
..._buildStyleSetting(),
..._buildSortSetting(),
..._buildLayoutSetting(),
..._buildColumnsSetting(),
..._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,6 +1,7 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.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/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -284,10 +285,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
columns: globalState.appController.columns,
sortNum: appState.sortNum,
proxies: group.all,
groupType: group.type,

View File

@@ -22,11 +22,80 @@ class GeoItem {
});
}
class Resources extends StatelessWidget {
class Resources extends StatefulWidget {
const Resources({super.key});
@override
Widget build(BuildContext context) {
State<Resources> createState() => _ResourcesState();
}
class _ResourcesState extends State<Resources> {
List<ExternalProvider> externalProviders = [];
List<GlobalObjectKey<_ProviderItemState>> providerItemKeys = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_syncExternalProviders();
});
}
_syncExternalProviders() async {
externalProviders = await clashCore.getExternalProviders();
if (mounted) {
setState(() {});
}
}
_updateProviders() async {
final updateProviders = providerItemKeys.map<Future>(
(key) async => await key.currentState?.updateProvider(false),
);
await Future.wait(updateProviders);
_syncExternalProviders();
}
List<Widget> _buildExternalProviderSection() {
List<GlobalObjectKey<_ProviderItemState>> keys = [];
final res = generateInfoSection(
info: Info(
iconData: Icons.source,
label: appLocalizations.externalResources,
),
actions: [
IconButton.filledTonal(
onPressed: () {
_updateProviders();
},
padding: const EdgeInsets.all(4),
iconSize: 20,
icon: const Icon(
Icons.sync,
),
)
],
items: externalProviders.map(
(externalProvider) {
final key =
GlobalObjectKey<_ProviderItemState>(externalProvider.name);
keys.add(key);
return ProviderItem(
key: key,
provider: externalProvider,
onUpdated: () {
_syncExternalProviders();
},
);
},
),
);
providerItemKeys = keys;
return res;
}
List<Widget> _buildGeoDataSection() {
const geoItems = <GeoItem>[
GeoItem(
label: "GeoIp",
@@ -42,19 +111,26 @@ class Resources extends StatelessWidget {
GeoItem(label: "ASN", fileName: asnFileName, key: "asn"),
];
return ListView.separated(
itemBuilder: (_, index) {
final geoItem = geoItems[index];
return GeoDataListItem(
return generateInfoSection(
info: Info(
iconData: Icons.storage,
label: appLocalizations.geoData,
),
items: geoItems.map(
(geoItem) => GeoDataListItem(
geoItem: geoItem,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: geoItems.length,
),
),
);
}
@override
Widget build(BuildContext context) {
return generateListView(
[
..._buildGeoDataSection(),
..._buildExternalProviderSection(),
],
);
}
}
@@ -91,6 +167,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
final appController = globalState.appController;
appController.clashConfig.geoXUrl =
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: geoItem.label,
@@ -149,6 +226,9 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
const SizedBox(
height: 8,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 12,
@@ -181,9 +261,9 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
updateGeoDateItem() async {
isUpdating.value = true;
try {
final message = await clashCore.updateGeoData(
geoName: geoItem.fileName,
geoType: geoItem.label,
final message = await clashCore.updateExternalProvider(
providerName: geoItem.fileName,
providerType: geoItem.label,
);
if (message.isNotEmpty) throw message;
} catch (e) {
@@ -235,6 +315,117 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
}
}
class ProviderItem extends StatefulWidget {
final ExternalProvider provider;
final Function onUpdated;
const ProviderItem({
super.key,
required this.provider,
required this.onUpdated,
});
@override
State<ProviderItem> createState() => _ProviderItemState();
}
class _ProviderItemState extends State<ProviderItem> {
final isUpdating = ValueNotifier<bool>(false);
ExternalProvider get provider => widget.provider;
_handleUpdateProfile() async {
await globalState.safeRun<void>(updateProvider);
widget.onUpdated();
}
updateProvider([isSingle = true]) async {
if (provider.vehicleType != "HTTP") return;
isUpdating.value = true;
try {
final message = await clashCore.updateExternalProvider(
providerName: provider.name,
providerType: provider.type,
);
if (message.isNotEmpty) throw message;
} catch (e) {
isUpdating.value = false;
if (!isSingle) {
return e.toString();
} else {
rethrow;
}
}
isUpdating.value = false;
return null;
}
String _buildProviderDesc() {
return "${provider.type} (${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}";
}
@override
void dispose() {
super.dispose();
isUpdating.dispose();
}
Widget _buildSubtitle() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
Text(
_buildProviderDesc(),
),
if (provider.vehicleType == "HTTP") ...[
const SizedBox(
height: 8,
),
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: () {
_handleUpdateProfile();
},
),
],
],
);
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: Text(provider.name),
subtitle: _buildSubtitle(),
trailing: SizedBox(
height: 48,
width: 48,
child: ValueListenableBuilder(
valueListenable: isUpdating,
builder: (_, isUpdating, ___) {
return FadeBox(
child: isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: const SizedBox(),
);
},
),
),
);
}
}
class UpdateGeoUrlFormDialog extends StatefulWidget {
final String title;
final String url;

View File

@@ -37,7 +37,7 @@
"overrideDesc": "Override Proxy related config",
"allowLan": "AllowLan",
"allowLanDesc": "Allow access proxy through the LAN",
"tun": "TUN",
"tun": "TUN mode",
"tunDesc": "only effective in administrator mode",
"minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Modify the default system exit event",
@@ -66,7 +66,6 @@
"hours": "Hours",
"days": "Days",
"minutes": "Minutes",
"seconds": "Seconds",
"ago": " Ago",
"just": "Just",
"qrcode": "QR code",
@@ -117,7 +116,7 @@
"logLevel": "LogLevel",
"show": "Show",
"exit": "Exit",
"systemProxy": "System proxy",
"systemProxy": "SystemProxy",
"project": "Project",
"core": "Core",
"tabAnimation": "Tab animation",
@@ -131,10 +130,12 @@
"notSelectedTip": "The current proxy group cannot be selected.",
"tip": "tip",
"backupAndRecovery": "Backup and Recovery",
"backupAndRecoveryDesc": "Sync data via WebDAV or file",
"backupAndRecoveryDesc": "Sync data by WebDAV",
"account": "Account",
"backup": "Backup",
"backupDesc": "Backup local data to WebDAV",
"recovery": "Recovery",
"recoveryDesc": "Recovery data from WebDAV",
"recoveryProfiles": "Only recovery profiles",
"recoveryAll": "Recovery all data",
"recoverySuccess": "Recovery success",
@@ -219,29 +220,5 @@
"onlyStatisticsProxy": "Only statistics proxy",
"onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic",
"deleteProfileTip": "Sure you want to delete the current profile?",
"prueBlackMode": "Prue black mode",
"keepAliveIntervalDesc": "Tcp keep alive interval",
"entries": " entries",
"local": "Local",
"remote": "Remote",
"remoteBackupDesc": "Backup local data to WebDAV",
"remoteRecoveryDesc": "Recovery data from WebDAV",
"localBackupDesc": "Backup local data to local",
"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"
"prueBlackMode": "Prue black mode"
}

View File

@@ -37,7 +37,7 @@
"overrideDesc": "覆写代理相关配置",
"allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理",
"tun": "虚拟网卡",
"tun": "TUN模式",
"tunDesc": "仅在管理员模式生效",
"minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件",
@@ -66,7 +66,6 @@
"hours": "小时",
"days": "天",
"minutes": "分钟",
"seconds": "秒",
"ago": "前",
"just": "刚刚",
"qrcode": "二维码",
@@ -131,10 +130,12 @@
"notSelectedTip": "当前代理组无法选中",
"tip": "提示",
"backupAndRecovery": "备份与恢复",
"backupAndRecoveryDesc": "通过WebDAV或者文件同步数据",
"backupAndRecoveryDesc": "通过WebDAV同步数据",
"account": "账号",
"backup": "备份",
"backupDesc": "备份数据到WebDAV",
"recovery": "恢复",
"recoveryDesc": "从WebDAV恢复数据",
"recoveryProfiles": "仅恢复配置文件",
"recoveryAll": "恢复所有数据",
"recoverySuccess": "恢复成功",
@@ -219,29 +220,5 @@
"onlyStatisticsProxy": "仅统计代理",
"onlyStatisticsProxyDesc": "开启后,将只统计代理流量",
"deleteProfileTip": "确定要删除当前配置吗?",
"prueBlackMode": "纯黑模式",
"keepAliveIntervalDesc": "TCP保持活动间隔",
"entries": "个条目",
"local": "本地",
"remote": "远程",
"remoteBackupDesc": "备份数据到WebDAV",
"remoteRecoveryDesc": "通过WebDAV恢复数据",
"localBackupDesc": "备份数据到本地",
"localRecoveryDesc": "通过文件恢复数据",
"mode": "模式",
"time": "时间",
"source": "来源",
"allApps": "所有应用",
"onlyOtherApps": "仅第三方应用",
"action": "操作",
"intelligentSelected": "智能选择",
"clipboardImport": "剪贴板导入",
"clipboardExport": "导出剪贴板",
"layout": "布局",
"tight": "宽松",
"standard": "标准",
"loose": "紧凑",
"profilesSort": "配置排序",
"start": "启动",
"stop": "暂停"
"prueBlackMode": "纯黑模式"
}

View File

@@ -33,7 +33,6 @@ class MessageLookup extends MessageLookupByLibrary {
"account": MessageLookupByLibrary.simpleMessage("Account"),
"accountTip":
MessageLookupByLibrary.simpleMessage("Account cannot be empty"),
"action": MessageLookupByLibrary.simpleMessage("Action"),
"add": MessageLookupByLibrary.simpleMessage("Add"),
"address": MessageLookupByLibrary.simpleMessage("Address"),
"addressHelp":
@@ -41,7 +40,6 @@ class MessageLookup extends MessageLookupByLibrary {
"addressTip": MessageLookupByLibrary.simpleMessage(
"Please enter a valid WebDAV address"),
"ago": MessageLookupByLibrary.simpleMessage(" Ago"),
"allApps": MessageLookupByLibrary.simpleMessage("All apps"),
"allowBypass": MessageLookupByLibrary.simpleMessage(
"Allow applications to bypass VPN"),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage(
@@ -76,8 +74,10 @@ class MessageLookup extends MessageLookupByLibrary {
"backup": MessageLookupByLibrary.simpleMessage("Backup"),
"backupAndRecovery":
MessageLookupByLibrary.simpleMessage("Backup and Recovery"),
"backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"Sync data via WebDAV or file"),
"backupAndRecoveryDesc":
MessageLookupByLibrary.simpleMessage("Sync data by WebDAV"),
"backupDesc":
MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"),
"backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"),
"bind": MessageLookupByLibrary.simpleMessage("Bind"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"),
@@ -91,10 +91,6 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdateError": MessageLookupByLibrary.simpleMessage(
"The current application is already the latest version"),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"clipboardExport":
MessageLookupByLibrary.simpleMessage("Export clipboard"),
"clipboardImport":
MessageLookupByLibrary.simpleMessage("Clipboard import"),
"columns": MessageLookupByLibrary.simpleMessage("Columns"),
"compatible":
MessageLookupByLibrary.simpleMessage("Compatibility mode"),
@@ -133,7 +129,6 @@ class MessageLookup extends MessageLookupByLibrary {
"download": MessageLookupByLibrary.simpleMessage("Download"),
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"),
"entries": MessageLookupByLibrary.simpleMessage(" entries"),
"exclude":
MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"),
"excludeDesc": MessageLookupByLibrary.simpleMessage(
@@ -173,37 +168,25 @@ class MessageLookup extends MessageLookupByLibrary {
"infiniteTime":
MessageLookupByLibrary.simpleMessage("Long term effective"),
"init": MessageLookupByLibrary.simpleMessage("Init"),
"intelligentSelected":
MessageLookupByLibrary.simpleMessage("Intelligent selection"),
"intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
"When turned on it will be able to receive IPv6 traffic"),
"just": MessageLookupByLibrary.simpleMessage("Just"),
"keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"layout": MessageLookupByLibrary.simpleMessage("Layout"),
"light": MessageLookupByLibrary.simpleMessage("Light"),
"list": MessageLookupByLibrary.simpleMessage("List"),
"local": MessageLookupByLibrary.simpleMessage("Local"),
"localBackupDesc":
MessageLookupByLibrary.simpleMessage("Backup local data to local"),
"localRecoveryDesc":
MessageLookupByLibrary.simpleMessage("Recovery data from file"),
"logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"),
"logcat": MessageLookupByLibrary.simpleMessage("Logcat"),
"logcatDesc": MessageLookupByLibrary.simpleMessage(
"Disabling will hide the log entry"),
"logs": MessageLookupByLibrary.simpleMessage("Logs"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"),
"loose": MessageLookupByLibrary.simpleMessage("Loose"),
"min": MessageLookupByLibrary.simpleMessage("Min"),
"minimizeOnExit":
MessageLookupByLibrary.simpleMessage("Minimize on exit"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
"Modify the default system exit event"),
"minutes": MessageLookupByLibrary.simpleMessage("Minutes"),
"mode": MessageLookupByLibrary.simpleMessage("Mode"),
"months": MessageLookupByLibrary.simpleMessage("Months"),
"more": MessageLookupByLibrary.simpleMessage("More"),
"name": MessageLookupByLibrary.simpleMessage("Name"),
@@ -227,8 +210,6 @@ class MessageLookup extends MessageLookupByLibrary {
"No profile, Please add a profile"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
"onlyOtherApps":
MessageLookupByLibrary.simpleMessage("Only third-party apps"),
"onlyStatisticsProxy":
MessageLookupByLibrary.simpleMessage("Only statistics proxy"),
"onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
@@ -268,7 +249,6 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"Please input the profile URL"),
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
"profilesSort": MessageLookupByLibrary.simpleMessage("Profiles sort"),
"project": MessageLookupByLibrary.simpleMessage("Project"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxiesSetting":
@@ -285,15 +265,12 @@ class MessageLookup extends MessageLookupByLibrary {
"recovery": MessageLookupByLibrary.simpleMessage("Recovery"),
"recoveryAll":
MessageLookupByLibrary.simpleMessage("Recovery all data"),
"recoveryDesc":
MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"),
"recoveryProfiles":
MessageLookupByLibrary.simpleMessage("Only recovery profiles"),
"recoverySuccess":
MessageLookupByLibrary.simpleMessage("Recovery success"),
"remote": MessageLookupByLibrary.simpleMessage("Remote"),
"remoteBackupDesc":
MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"),
"remoteRecoveryDesc":
MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"),
"requests": MessageLookupByLibrary.simpleMessage("Requests"),
"requestsDesc": MessageLookupByLibrary.simpleMessage(
"View recently request records"),
@@ -303,7 +280,6 @@ class MessageLookup extends MessageLookupByLibrary {
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"seconds": MessageLookupByLibrary.simpleMessage("Seconds"),
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
"selected": MessageLookupByLibrary.simpleMessage("Selected"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
@@ -314,16 +290,12 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Start in the background"),
"size": MessageLookupByLibrary.simpleMessage("Size"),
"sort": MessageLookupByLibrary.simpleMessage("Sort"),
"source": MessageLookupByLibrary.simpleMessage("Source"),
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
"start": MessageLookupByLibrary.simpleMessage("Start"),
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"style": MessageLookupByLibrary.simpleMessage("Style"),
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
"tab": MessageLookupByLibrary.simpleMessage("Tab"),
@@ -340,12 +312,10 @@ class MessageLookup extends MessageLookupByLibrary {
"Set dark mode,adjust the color"),
"themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"),
"threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"),
"tight": MessageLookupByLibrary.simpleMessage("Tight"),
"time": MessageLookupByLibrary.simpleMessage("Time"),
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN"),
"tun": MessageLookupByLibrary.simpleMessage("TUN mode"),
"tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"),
"twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"),

View File

@@ -31,13 +31,11 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"),
"account": MessageLookupByLibrary.simpleMessage("账号"),
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
"action": MessageLookupByLibrary.simpleMessage("操作"),
"add": MessageLookupByLibrary.simpleMessage("添加"),
"address": MessageLookupByLibrary.simpleMessage("地址"),
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"ago": MessageLookupByLibrary.simpleMessage(""),
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
"allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
@@ -64,7 +62,8 @@ class MessageLookup extends MessageLookupByLibrary {
"backup": MessageLookupByLibrary.simpleMessage("备份"),
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
"backupAndRecoveryDesc":
MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"),
MessageLookupByLibrary.simpleMessage("通过WebDAV同步数据"),
"backupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
"bind": MessageLookupByLibrary.simpleMessage("绑定"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
@@ -75,8 +74,6 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
"columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc":
@@ -109,7 +106,6 @@ class MessageLookup extends MessageLookupByLibrary {
"download": MessageLookupByLibrary.simpleMessage("下载"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"entries": MessageLookupByLibrary.simpleMessage("个条目"),
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
"excludeDesc":
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
@@ -140,31 +136,22 @@ class MessageLookup extends MessageLookupByLibrary {
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"layout": MessageLookupByLibrary.simpleMessage("布局"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"),
"local": MessageLookupByLibrary.simpleMessage("本地"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"loose": MessageLookupByLibrary.simpleMessage("紧凑"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc":
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
"minutes": MessageLookupByLibrary.simpleMessage("分钟"),
"mode": MessageLookupByLibrary.simpleMessage("模式"),
"months": MessageLookupByLibrary.simpleMessage(""),
"more": MessageLookupByLibrary.simpleMessage("更多"),
"name": MessageLookupByLibrary.simpleMessage("名称"),
@@ -184,7 +171,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
"onlyStatisticsProxyDesc":
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"),
@@ -216,7 +202,6 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入配置URL"),
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
@@ -228,12 +213,9 @@ class MessageLookup extends MessageLookupByLibrary {
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
"recoveryDesc": MessageLookupByLibrary.simpleMessage("从WebDAV恢复数据"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"remote": MessageLookupByLibrary.simpleMessage("远程"),
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
"remoteRecoveryDesc":
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
"requests": MessageLookupByLibrary.simpleMessage("请求"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
"resources": MessageLookupByLibrary.simpleMessage("资源"),
@@ -241,7 +223,6 @@ class MessageLookup extends MessageLookupByLibrary {
"rule": MessageLookupByLibrary.simpleMessage("规则"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("搜索"),
"seconds": MessageLookupByLibrary.simpleMessage(""),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"),
@@ -251,11 +232,7 @@ class MessageLookup extends MessageLookupByLibrary {
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
"sort": MessageLookupByLibrary.simpleMessage("排序"),
"source": MessageLookupByLibrary.simpleMessage("来源"),
"standard": MessageLookupByLibrary.simpleMessage("标准"),
"start": MessageLookupByLibrary.simpleMessage("启动"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"stop": MessageLookupByLibrary.simpleMessage("暂停"),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"style": MessageLookupByLibrary.simpleMessage("风格"),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
@@ -275,12 +252,10 @@ class MessageLookup extends MessageLookupByLibrary {
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tight": MessageLookupByLibrary.simpleMessage("宽松"),
"time": MessageLookupByLibrary.simpleMessage("时间"),
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"tun": MessageLookupByLibrary.simpleMessage("TUN模式"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"unableToUpdateCurrentProfileDesc":

View File

@@ -430,10 +430,10 @@ class AppLocalizations {
);
}
/// `TUN`
/// `TUN mode`
String get tun {
return Intl.message(
'TUN',
'TUN mode',
name: 'tun',
desc: '',
args: [],
@@ -720,16 +720,6 @@ class AppLocalizations {
);
}
/// `Seconds`
String get seconds {
return Intl.message(
'Seconds',
name: 'seconds',
desc: '',
args: [],
);
}
/// ` Ago`
String get ago {
return Intl.message(
@@ -1230,10 +1220,10 @@ class AppLocalizations {
);
}
/// `System proxy`
/// `SystemProxy`
String get systemProxy {
return Intl.message(
'System proxy',
'SystemProxy',
name: 'systemProxy',
desc: '',
args: [],
@@ -1370,10 +1360,10 @@ class AppLocalizations {
);
}
/// `Sync data via WebDAV or file`
/// `Sync data by WebDAV`
String get backupAndRecoveryDesc {
return Intl.message(
'Sync data via WebDAV or file',
'Sync data by WebDAV',
name: 'backupAndRecoveryDesc',
desc: '',
args: [],
@@ -1400,6 +1390,16 @@ class AppLocalizations {
);
}
/// `Backup local data to WebDAV`
String get backupDesc {
return Intl.message(
'Backup local data to WebDAV',
name: 'backupDesc',
desc: '',
args: [],
);
}
/// `Recovery`
String get recovery {
return Intl.message(
@@ -1410,6 +1410,16 @@ class AppLocalizations {
);
}
/// `Recovery data from WebDAV`
String get recoveryDesc {
return Intl.message(
'Recovery data from WebDAV',
name: 'recoveryDesc',
desc: '',
args: [],
);
}
/// `Only recovery profiles`
String get recoveryProfiles {
return Intl.message(
@@ -2259,246 +2269,6 @@ class AppLocalizations {
args: [],
);
}
/// `Tcp keep alive interval`
String get keepAliveIntervalDesc {
return Intl.message(
'Tcp keep alive interval',
name: 'keepAliveIntervalDesc',
desc: '',
args: [],
);
}
/// ` entries`
String get entries {
return Intl.message(
' entries',
name: 'entries',
desc: '',
args: [],
);
}
/// `Local`
String get local {
return Intl.message(
'Local',
name: 'local',
desc: '',
args: [],
);
}
/// `Remote`
String get remote {
return Intl.message(
'Remote',
name: 'remote',
desc: '',
args: [],
);
}
/// `Backup local data to WebDAV`
String get remoteBackupDesc {
return Intl.message(
'Backup local data to WebDAV',
name: 'remoteBackupDesc',
desc: '',
args: [],
);
}
/// `Recovery data from WebDAV`
String get remoteRecoveryDesc {
return Intl.message(
'Recovery data from WebDAV',
name: 'remoteRecoveryDesc',
desc: '',
args: [],
);
}
/// `Backup local data to local`
String get localBackupDesc {
return Intl.message(
'Backup local data to local',
name: 'localBackupDesc',
desc: '',
args: [],
);
}
/// `Recovery data from file`
String get localRecoveryDesc {
return Intl.message(
'Recovery data from file',
name: 'localRecoveryDesc',
desc: '',
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> {

View File

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

View File

@@ -8,7 +8,6 @@ import 'connection.dart';
import 'ffi.dart';
import 'log.dart';
import 'navigation.dart';
import 'package.dart';
import 'profile.dart';
import 'proxy.dart';
import 'system_color_scheme.dart';
@@ -36,8 +35,6 @@ class AppState with ChangeNotifier {
double _viewWidth;
List<Connection> _requests;
num _checkIpNum;
List<ExternalProvider> _providers;
List<Package> _packages;
AppState({
required Mode mode,
@@ -57,8 +54,6 @@ class AppState with ChangeNotifier {
_totalTraffic = Traffic(),
_delayMap = {},
_groups = [],
_providers = [],
_packages = [],
_isCompatible = isCompatible,
_systemColorSchemes = const SystemColorSchemes();
@@ -335,32 +330,6 @@ class AppState with ChangeNotifier {
}
}
List<Package> get packages => _packages;
set packages(List<Package> value) {
if (!const ListEquality<Package>().equals(_packages, value)) {
_packages = value;
notifyListeners();
}
}
List<ExternalProvider> get providers => _providers;
set providers(List<ExternalProvider> value) {
if (!const ListEquality<ExternalProvider>().equals(_providers, value)) {
_providers = value;
notifyListeners();
}
}
setProvider(ExternalProvider? provider) {
if(provider == null) return;
final index = _providers.indexWhere((item) => item.name == provider.name);
if (index == -1) return;
_providers = List.from(_providers)..[index] = provider;
notifyListeners();
}
Group? getGroupWithName(String groupName) {
final index =
currentGroups.indexWhere((element) => element.name == groupName);

View File

@@ -119,7 +119,6 @@ class ClashConfig extends ChangeNotifier {
String _externalController;
Mode _mode;
FindProcessMode _findProcessMode;
int _keepAliveInterval;
bool _unifiedDelay;
bool _tcpConcurrent;
Tun _tun;
@@ -140,7 +139,6 @@ class ClashConfig extends ChangeNotifier {
_unifiedDelay = false,
_geodataLoader = geodataLoaderMemconservative,
_externalController = '',
_keepAliveInterval = 30,
_dns = Dns(),
_geoXUrl = defaultGeoXMap,
_rules = [];
@@ -205,16 +203,6 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(name: "keep-alive-interval", defaultValue: 30)
int get keepAliveInterval => _keepAliveInterval;
set keepAliveInterval(int value) {
if (_keepAliveInterval != value) {
_keepAliveInterval = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false)
bool get ipv6 => _ipv6;

View File

@@ -18,7 +18,6 @@ class AccessControl with _$AccessControl {
@Default(AccessControlMode.rejectSelected) AccessControlMode mode,
@Default([]) List<String> acceptList,
@Default([]) List<String> rejectList,
@Default(AccessSortType.none) AccessSortType sort,
@Default(true) bool isFilterSystemApp,
}) = _AccessControl;
@@ -26,27 +25,17 @@ class AccessControl with _$AccessControl {
_$AccessControlFromJson(json);
}
extension AccessControlExt on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
@freezed
class CoreState with _$CoreState {
const factory CoreState({
AccessControl? accessControl,
required String currentProfileName,
required bool enable,
required bool allowBypass,
required bool systemProxy,
required int mixedPort,
required bool onlyProxy,
}) = _CoreState;
factory CoreState.fromJson(Map<String, Object?> json) =>
_$CoreStateFromJson(json);
factory CoreState.fromJson(Map<String, Object?> json) => _$CoreStateFromJson(json);
}
@freezed
@@ -59,30 +48,10 @@ class WindowProps with _$WindowProps {
}) = _WindowProps;
factory WindowProps.fromJson(Map<String, Object?>? json) =>
json == null ? const WindowProps() : _$WindowPropsFromJson(json);
json == null ? defaultWindowProps : _$WindowPropsFromJson(json);
}
@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);
}
const defaultWindowProps = WindowProps();
@JsonSerializable()
class Config extends ChangeNotifier {
@@ -102,19 +71,18 @@ class Config extends ChangeNotifier {
AccessControl _accessControl;
bool _isAnimateToPage;
bool _autoCheckUpdate;
bool _allowBypass;
bool _systemProxy;
bool _isExclude;
DAV? _dav;
bool _isCloseConnections;
ProxiesType _proxiesType;
ProxyCardType _proxyCardType;
ProxiesLayout _proxiesLayout;
int _proxiesColumns;
String _testUrl;
WindowProps _windowProps;
bool _onlyProxy;
bool _prueBlack;
VpnProps _vpnProps;
DesktopProps _desktopProps;
bool _showLabel;
Config()
: _profiles = [],
@@ -130,19 +98,18 @@ class Config extends ChangeNotifier {
_isMinimizeOnExit = true,
_isAccessControl = false,
_autoCheckUpdate = true,
_systemProxy = false,
_testUrl = defaultTestUrl,
_accessControl = const AccessControl(),
_isAnimateToPage = true,
_allowBypass = true,
_isExclude = false,
_proxyCardType = ProxyCardType.expand,
_windowProps = const WindowProps(),
_windowProps = defaultWindowProps,
_proxiesType = ProxiesType.tab,
_proxiesColumns = 2,
_prueBlack = false,
_onlyProxy = false,
_proxiesLayout = ProxiesLayout.standard,
_vpnProps = const VpnProps(),
_desktopProps = const DesktopProps(),
_showLabel = false;
_onlyProxy = false;
deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList();
@@ -344,16 +311,6 @@ 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)
bool get isMinimizeOnExit => _isMinimizeOnExit;
@@ -432,6 +389,30 @@ 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)
bool get onlyProxy {
return _onlyProxy;
@@ -491,6 +472,16 @@ 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)
String get testUrl => _testUrl;
@@ -520,34 +511,6 @@ 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([
Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -572,6 +535,7 @@ class Config extends ChangeNotifier {
_openLog = config._openLog;
_themeMode = config._themeMode;
_locale = config._locale;
_allowBypass = config._allowBypass;
_primaryColor = config._primaryColor;
_proxiesSortType = config._proxiesSortType;
_isMinimizeOnExit = config._isMinimizeOnExit;
@@ -583,8 +547,6 @@ class Config extends ChangeNotifier {
_testUrl = config._testUrl;
_isExclude = config._isExclude;
_windowProps = config._windowProps;
_vpnProps = config._vpnProps;
_desktopProps = config._desktopProps;
}
notifyListeners();
}

View File

@@ -24,7 +24,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams {
@freezed
class UpdateConfigParams with _$UpdateConfigParams {
const factory UpdateConfigParams({
@JsonKey(name: "profile-id") required String profileId,
@JsonKey(name: "profile-path") String? profilePath,
required ClashConfig config,
required ConfigExtendedParams params,
}) = _UpdateConfigParams;
@@ -123,9 +123,6 @@ class ExternalProvider with _$ExternalProvider {
const factory ExternalProvider({
required String name,
required String type,
required String path,
required int count,
@Default(false) bool isUpdating,
@JsonKey(name: "vehicle-type") required String vehicleType,
@JsonKey(name: "update-at") required DateTime updateAt,
}) = _ExternalProvider;
@@ -143,7 +140,7 @@ abstract mixin class AppMessageListener {
void onStarted(String runTime) {}
void onLoaded(String providerName) {}
void onLoaded(String groupName) {}
}
abstract mixin class ServiceMessageListener {
@@ -153,5 +150,7 @@ abstract mixin class ServiceMessageListener {
onStarted(String runTime) {}
onLoaded(String providerName) {}
onLoaded(String groupName) {}
}

View File

@@ -45,7 +45,6 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
..logLevel =
$enumDecodeNullable(_$LogLevelEnumMap, json['log-level']) ?? LogLevel.info
..externalController = json['external-controller'] as String? ?? ''
..keepAliveInterval = (json['keep-alive-interval'] as num?)?.toInt() ?? 30
..ipv6 = json['ipv6'] as bool? ?? false
..geodataLoader = json['geodata-loader'] as String? ?? 'memconservative'
..unifiedDelay = json['unified-delay'] as bool? ?? false
@@ -76,7 +75,6 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'allow-lan': instance.allowLan,
'log-level': _$LogLevelEnumMap[instance.logLevel]!,
'external-controller': instance.externalController,
'keep-alive-interval': instance.keepAliveInterval,
'ipv6': instance.ipv6,
'geodata-loader': instance.geodataLoader,
'unified-delay': instance.unifiedDelay,

View File

@@ -23,7 +23,6 @@ mixin _$AccessControl {
AccessControlMode get mode => throw _privateConstructorUsedError;
List<String> get acceptList => throw _privateConstructorUsedError;
List<String> get rejectList => throw _privateConstructorUsedError;
AccessSortType get sort => throw _privateConstructorUsedError;
bool get isFilterSystemApp => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -42,7 +41,6 @@ abstract class $AccessControlCopyWith<$Res> {
{AccessControlMode mode,
List<String> acceptList,
List<String> rejectList,
AccessSortType sort,
bool isFilterSystemApp});
}
@@ -62,7 +60,6 @@ class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl>
Object? mode = null,
Object? acceptList = null,
Object? rejectList = null,
Object? sort = null,
Object? isFilterSystemApp = null,
}) {
return _then(_value.copyWith(
@@ -78,10 +75,6 @@ class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl>
? _value.rejectList
: rejectList // ignore: cast_nullable_to_non_nullable
as List<String>,
sort: null == sort
? _value.sort
: sort // ignore: cast_nullable_to_non_nullable
as AccessSortType,
isFilterSystemApp: null == isFilterSystemApp
? _value.isFilterSystemApp
: isFilterSystemApp // ignore: cast_nullable_to_non_nullable
@@ -102,7 +95,6 @@ abstract class _$$AccessControlImplCopyWith<$Res>
{AccessControlMode mode,
List<String> acceptList,
List<String> rejectList,
AccessSortType sort,
bool isFilterSystemApp});
}
@@ -120,7 +112,6 @@ class __$$AccessControlImplCopyWithImpl<$Res>
Object? mode = null,
Object? acceptList = null,
Object? rejectList = null,
Object? sort = null,
Object? isFilterSystemApp = null,
}) {
return _then(_$AccessControlImpl(
@@ -136,10 +127,6 @@ class __$$AccessControlImplCopyWithImpl<$Res>
? _value._rejectList
: rejectList // ignore: cast_nullable_to_non_nullable
as List<String>,
sort: null == sort
? _value.sort
: sort // ignore: cast_nullable_to_non_nullable
as AccessSortType,
isFilterSystemApp: null == isFilterSystemApp
? _value.isFilterSystemApp
: isFilterSystemApp // ignore: cast_nullable_to_non_nullable
@@ -155,7 +142,6 @@ class _$AccessControlImpl implements _AccessControl {
{this.mode = AccessControlMode.rejectSelected,
final List<String> acceptList = const [],
final List<String> rejectList = const [],
this.sort = AccessSortType.none,
this.isFilterSystemApp = true})
: _acceptList = acceptList,
_rejectList = rejectList;
@@ -184,16 +170,13 @@ class _$AccessControlImpl implements _AccessControl {
return EqualUnmodifiableListView(_rejectList);
}
@override
@JsonKey()
final AccessSortType sort;
@override
@JsonKey()
final bool isFilterSystemApp;
@override
String toString() {
return 'AccessControl(mode: $mode, acceptList: $acceptList, rejectList: $rejectList, sort: $sort, isFilterSystemApp: $isFilterSystemApp)';
return 'AccessControl(mode: $mode, acceptList: $acceptList, rejectList: $rejectList, isFilterSystemApp: $isFilterSystemApp)';
}
@override
@@ -206,7 +189,6 @@ class _$AccessControlImpl implements _AccessControl {
.equals(other._acceptList, _acceptList) &&
const DeepCollectionEquality()
.equals(other._rejectList, _rejectList) &&
(identical(other.sort, sort) || other.sort == sort) &&
(identical(other.isFilterSystemApp, isFilterSystemApp) ||
other.isFilterSystemApp == isFilterSystemApp));
}
@@ -218,7 +200,6 @@ class _$AccessControlImpl implements _AccessControl {
mode,
const DeepCollectionEquality().hash(_acceptList),
const DeepCollectionEquality().hash(_rejectList),
sort,
isFilterSystemApp);
@JsonKey(ignore: true)
@@ -240,7 +221,6 @@ abstract class _AccessControl implements AccessControl {
{final AccessControlMode mode,
final List<String> acceptList,
final List<String> rejectList,
final AccessSortType sort,
final bool isFilterSystemApp}) = _$AccessControlImpl;
factory _AccessControl.fromJson(Map<String, dynamic> json) =
@@ -253,8 +233,6 @@ abstract class _AccessControl implements AccessControl {
@override
List<String> get rejectList;
@override
AccessSortType get sort;
@override
bool get isFilterSystemApp;
@override
@JsonKey(ignore: true)
@@ -269,8 +247,6 @@ CoreState _$CoreStateFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$CoreState {
AccessControl? get accessControl => throw _privateConstructorUsedError;
String get currentProfileName => throw _privateConstructorUsedError;
bool get enable => throw _privateConstructorUsedError;
bool get allowBypass => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError;
int get mixedPort => throw _privateConstructorUsedError;
@@ -289,8 +265,6 @@ abstract class $CoreStateCopyWith<$Res> {
@useResult
$Res call(
{AccessControl? accessControl,
String currentProfileName,
bool enable,
bool allowBypass,
bool systemProxy,
int mixedPort,
@@ -313,8 +287,6 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
@override
$Res call({
Object? accessControl = freezed,
Object? currentProfileName = null,
Object? enable = null,
Object? allowBypass = null,
Object? systemProxy = null,
Object? mixedPort = null,
@@ -325,14 +297,6 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl?,
currentProfileName: null == currentProfileName
? _value.currentProfileName
: currentProfileName // ignore: cast_nullable_to_non_nullable
as String,
enable: null == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as bool,
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
@@ -375,8 +339,6 @@ abstract class _$$CoreStateImplCopyWith<$Res>
@useResult
$Res call(
{AccessControl? accessControl,
String currentProfileName,
bool enable,
bool allowBypass,
bool systemProxy,
int mixedPort,
@@ -398,8 +360,6 @@ class __$$CoreStateImplCopyWithImpl<$Res>
@override
$Res call({
Object? accessControl = freezed,
Object? currentProfileName = null,
Object? enable = null,
Object? allowBypass = null,
Object? systemProxy = null,
Object? mixedPort = null,
@@ -410,14 +370,6 @@ class __$$CoreStateImplCopyWithImpl<$Res>
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl?,
currentProfileName: null == currentProfileName
? _value.currentProfileName
: currentProfileName // ignore: cast_nullable_to_non_nullable
as String,
enable: null == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as bool,
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
@@ -443,8 +395,6 @@ class __$$CoreStateImplCopyWithImpl<$Res>
class _$CoreStateImpl implements _CoreState {
const _$CoreStateImpl(
{this.accessControl,
required this.currentProfileName,
required this.enable,
required this.allowBypass,
required this.systemProxy,
required this.mixedPort,
@@ -456,10 +406,6 @@ class _$CoreStateImpl implements _CoreState {
@override
final AccessControl? accessControl;
@override
final String currentProfileName;
@override
final bool enable;
@override
final bool allowBypass;
@override
final bool systemProxy;
@@ -470,7 +416,7 @@ class _$CoreStateImpl implements _CoreState {
@override
String toString() {
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
return 'CoreState(accessControl: $accessControl, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
}
@override
@@ -480,9 +426,6 @@ class _$CoreStateImpl implements _CoreState {
other is _$CoreStateImpl &&
(identical(other.accessControl, accessControl) ||
other.accessControl == accessControl) &&
(identical(other.currentProfileName, currentProfileName) ||
other.currentProfileName == currentProfileName) &&
(identical(other.enable, enable) || other.enable == enable) &&
(identical(other.allowBypass, allowBypass) ||
other.allowBypass == allowBypass) &&
(identical(other.systemProxy, systemProxy) ||
@@ -495,15 +438,8 @@ class _$CoreStateImpl implements _CoreState {
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
accessControl,
currentProfileName,
enable,
allowBypass,
systemProxy,
mixedPort,
onlyProxy);
int get hashCode => Object.hash(runtimeType, accessControl, allowBypass,
systemProxy, mixedPort, onlyProxy);
@JsonKey(ignore: true)
@override
@@ -522,8 +458,6 @@ class _$CoreStateImpl implements _CoreState {
abstract class _CoreState implements CoreState {
const factory _CoreState(
{final AccessControl? accessControl,
required final String currentProfileName,
required final bool enable,
required final bool allowBypass,
required final bool systemProxy,
required final int mixedPort,
@@ -535,10 +469,6 @@ abstract class _CoreState implements CoreState {
@override
AccessControl? get accessControl;
@override
String get currentProfileName;
@override
bool get enable;
@override
bool get allowBypass;
@override
bool get systemProxy;
@@ -742,318 +672,3 @@ abstract class _WindowProps implements WindowProps {
_$$WindowPropsImplCopyWith<_$WindowPropsImpl> get copyWith =>
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,9 +23,6 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..proxiesSortType =
$enumDecodeNullable(_$ProxiesSortTypeEnumMap, json['proxiesSortType']) ??
ProxiesSortType.none
..proxiesLayout =
$enumDecodeNullable(_$ProxiesLayoutEnumMap, json['proxiesLayout']) ??
ProxiesLayout.standard
..isMinimizeOnExit = json['isMinimizeOnExit'] as bool? ?? true
..isAccessControl = json['isAccessControl'] as bool? ?? false
..accessControl =
@@ -36,6 +33,8 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
..isCompatible = json['isCompatible'] 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
..prueBlack = json['prueBlack'] as bool? ?? false
..isCloseConnections = json['isCloseConnections'] as bool? ?? false
@@ -45,15 +44,12 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2
..testUrl =
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204'
..isExclude = json['isExclude'] as bool? ?? false
..windowProps =
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;
WindowProps.fromJson(json['windowProps'] as Map<String, dynamic>?);
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -66,7 +62,6 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'locale': instance.locale,
'primaryColor': instance.primaryColor,
'proxiesSortType': _$ProxiesSortTypeEnumMap[instance.proxiesSortType]!,
'proxiesLayout': _$ProxiesLayoutEnumMap[instance.proxiesLayout]!,
'isMinimizeOnExit': instance.isMinimizeOnExit,
'isAccessControl': instance.isAccessControl,
'accessControl': instance.accessControl,
@@ -74,17 +69,17 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'isAnimateToPage': instance.isAnimateToPage,
'isCompatible': instance.isCompatible,
'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
'onlyProxy': instance.onlyProxy,
'prueBlack': instance.prueBlack,
'isCloseConnections': instance.isCloseConnections,
'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!,
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
'proxiesColumns': instance.proxiesColumns,
'test-url': instance.testUrl,
'isExclude': instance.isExclude,
'windowProps': instance.windowProps,
'vpnProps': instance.vpnProps,
'desktopProps': instance.desktopProps,
'showLabel': instance.showLabel,
};
const _$ThemeModeEnumMap = {
@@ -99,12 +94,6 @@ const _$ProxiesSortTypeEnumMap = {
ProxiesSortType.name: 'name',
};
const _$ProxiesLayoutEnumMap = {
ProxiesLayout.loose: 'loose',
ProxiesLayout.standard: 'standard',
ProxiesLayout.tight: 'tight',
};
const _$ProxiesTypeEnumMap = {
ProxiesType.tab: 'tab',
ProxiesType.list: 'list',
@@ -128,8 +117,6 @@ _$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) =>
?.map((e) => e as String)
.toList() ??
const [],
sort: $enumDecodeNullable(_$AccessSortTypeEnumMap, json['sort']) ??
AccessSortType.none,
isFilterSystemApp: json['isFilterSystemApp'] as bool? ?? true,
);
@@ -138,7 +125,6 @@ Map<String, dynamic> _$$AccessControlImplToJson(_$AccessControlImpl instance) =>
'mode': _$AccessControlModeEnumMap[instance.mode]!,
'acceptList': instance.acceptList,
'rejectList': instance.rejectList,
'sort': _$AccessSortTypeEnumMap[instance.sort]!,
'isFilterSystemApp': instance.isFilterSystemApp,
};
@@ -147,20 +133,12 @@ const _$AccessControlModeEnumMap = {
AccessControlMode.rejectSelected: 'rejectSelected',
};
const _$AccessSortTypeEnumMap = {
AccessSortType.none: 'none',
AccessSortType.name: 'name',
AccessSortType.time: 'time',
};
_$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
_$CoreStateImpl(
accessControl: json['accessControl'] == null
? null
: AccessControl.fromJson(
json['accessControl'] as Map<String, dynamic>),
currentProfileName: json['currentProfileName'] as String,
enable: json['enable'] as bool,
allowBypass: json['allowBypass'] as bool,
systemProxy: json['systemProxy'] as bool,
mixedPort: (json['mixedPort'] as num).toInt(),
@@ -170,8 +148,6 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
<String, dynamic>{
'accessControl': instance.accessControl,
'currentProfileName': instance.currentProfileName,
'enable': instance.enable,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
'mixedPort': instance.mixedPort,
@@ -193,27 +169,3 @@ Map<String, dynamic> _$$WindowPropsImplToJson(_$WindowPropsImpl instance) =>
'top': instance.top,
'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

@@ -248,8 +248,8 @@ UpdateConfigParams _$UpdateConfigParamsFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$UpdateConfigParams {
@JsonKey(name: "profile-id")
String get profileId => throw _privateConstructorUsedError;
@JsonKey(name: "profile-path")
String? get profilePath => throw _privateConstructorUsedError;
ClashConfig get config => throw _privateConstructorUsedError;
ConfigExtendedParams get params => throw _privateConstructorUsedError;
@@ -266,7 +266,7 @@ abstract class $UpdateConfigParamsCopyWith<$Res> {
_$UpdateConfigParamsCopyWithImpl<$Res, UpdateConfigParams>;
@useResult
$Res call(
{@JsonKey(name: "profile-id") String profileId,
{@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config,
ConfigExtendedParams params});
@@ -286,15 +286,15 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? profileId = null,
Object? profilePath = freezed,
Object? config = null,
Object? params = null,
}) {
return _then(_value.copyWith(
profileId: null == profileId
? _value.profileId
: profileId // ignore: cast_nullable_to_non_nullable
as String,
profilePath: freezed == profilePath
? _value.profilePath
: profilePath // ignore: cast_nullable_to_non_nullable
as String?,
config: null == config
? _value.config
: config // ignore: cast_nullable_to_non_nullable
@@ -324,7 +324,7 @@ abstract class _$$UpdateConfigParamsImplCopyWith<$Res>
@override
@useResult
$Res call(
{@JsonKey(name: "profile-id") String profileId,
{@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config,
ConfigExtendedParams params});
@@ -343,15 +343,15 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? profileId = null,
Object? profilePath = freezed,
Object? config = null,
Object? params = null,
}) {
return _then(_$UpdateConfigParamsImpl(
profileId: null == profileId
? _value.profileId
: profileId // ignore: cast_nullable_to_non_nullable
as String,
profilePath: freezed == profilePath
? _value.profilePath
: profilePath // ignore: cast_nullable_to_non_nullable
as String?,
config: null == config
? _value.config
: config // ignore: cast_nullable_to_non_nullable
@@ -368,7 +368,7 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
@JsonSerializable()
class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
const _$UpdateConfigParamsImpl(
{@JsonKey(name: "profile-id") required this.profileId,
{@JsonKey(name: "profile-path") this.profilePath,
required this.config,
required this.params});
@@ -376,8 +376,8 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
_$$UpdateConfigParamsImplFromJson(json);
@override
@JsonKey(name: "profile-id")
final String profileId;
@JsonKey(name: "profile-path")
final String? profilePath;
@override
final ClashConfig config;
@override
@@ -385,7 +385,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
@override
String toString() {
return 'UpdateConfigParams(profileId: $profileId, config: $config, params: $params)';
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, params: $params)';
}
@override
@@ -393,15 +393,15 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UpdateConfigParamsImpl &&
(identical(other.profileId, profileId) ||
other.profileId == profileId) &&
(identical(other.profilePath, profilePath) ||
other.profilePath == profilePath) &&
(identical(other.config, config) || other.config == config) &&
(identical(other.params, params) || other.params == params));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, profileId, config, params);
int get hashCode => Object.hash(runtimeType, profilePath, config, params);
@JsonKey(ignore: true)
@override
@@ -420,7 +420,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
abstract class _UpdateConfigParams implements UpdateConfigParams {
const factory _UpdateConfigParams(
{@JsonKey(name: "profile-id") required final String profileId,
{@JsonKey(name: "profile-path") final String? profilePath,
required final ClashConfig config,
required final ConfigExtendedParams params}) = _$UpdateConfigParamsImpl;
@@ -428,8 +428,8 @@ abstract class _UpdateConfigParams implements UpdateConfigParams {
_$UpdateConfigParamsImpl.fromJson;
@override
@JsonKey(name: "profile-id")
String get profileId;
@JsonKey(name: "profile-path")
String? get profilePath;
@override
ClashConfig get config;
@override
@@ -1687,9 +1687,6 @@ ExternalProvider _$ExternalProviderFromJson(Map<String, dynamic> json) {
mixin _$ExternalProvider {
String get name => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
String get path => throw _privateConstructorUsedError;
int get count => throw _privateConstructorUsedError;
bool get isUpdating => throw _privateConstructorUsedError;
@JsonKey(name: "vehicle-type")
String get vehicleType => throw _privateConstructorUsedError;
@JsonKey(name: "update-at")
@@ -1710,9 +1707,6 @@ abstract class $ExternalProviderCopyWith<$Res> {
$Res call(
{String name,
String type,
String path,
int count,
bool isUpdating,
@JsonKey(name: "vehicle-type") String vehicleType,
@JsonKey(name: "update-at") DateTime updateAt});
}
@@ -1732,9 +1726,6 @@ class _$ExternalProviderCopyWithImpl<$Res, $Val extends ExternalProvider>
$Res call({
Object? name = null,
Object? type = null,
Object? path = null,
Object? count = null,
Object? isUpdating = null,
Object? vehicleType = null,
Object? updateAt = null,
}) {
@@ -1747,18 +1738,6 @@ class _$ExternalProviderCopyWithImpl<$Res, $Val extends ExternalProvider>
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
path: null == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as String,
count: null == count
? _value.count
: count // ignore: cast_nullable_to_non_nullable
as int,
isUpdating: null == isUpdating
? _value.isUpdating
: isUpdating // ignore: cast_nullable_to_non_nullable
as bool,
vehicleType: null == vehicleType
? _value.vehicleType
: vehicleType // ignore: cast_nullable_to_non_nullable
@@ -1782,9 +1761,6 @@ abstract class _$$ExternalProviderImplCopyWith<$Res>
$Res call(
{String name,
String type,
String path,
int count,
bool isUpdating,
@JsonKey(name: "vehicle-type") String vehicleType,
@JsonKey(name: "update-at") DateTime updateAt});
}
@@ -1802,9 +1778,6 @@ class __$$ExternalProviderImplCopyWithImpl<$Res>
$Res call({
Object? name = null,
Object? type = null,
Object? path = null,
Object? count = null,
Object? isUpdating = null,
Object? vehicleType = null,
Object? updateAt = null,
}) {
@@ -1817,18 +1790,6 @@ class __$$ExternalProviderImplCopyWithImpl<$Res>
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
path: null == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as String,
count: null == count
? _value.count
: count // ignore: cast_nullable_to_non_nullable
as int,
isUpdating: null == isUpdating
? _value.isUpdating
: isUpdating // ignore: cast_nullable_to_non_nullable
as bool,
vehicleType: null == vehicleType
? _value.vehicleType
: vehicleType // ignore: cast_nullable_to_non_nullable
@@ -1847,9 +1808,6 @@ class _$ExternalProviderImpl implements _ExternalProvider {
const _$ExternalProviderImpl(
{required this.name,
required this.type,
required this.path,
required this.count,
this.isUpdating = false,
@JsonKey(name: "vehicle-type") required this.vehicleType,
@JsonKey(name: "update-at") required this.updateAt});
@@ -1861,13 +1819,6 @@ class _$ExternalProviderImpl implements _ExternalProvider {
@override
final String type;
@override
final String path;
@override
final int count;
@override
@JsonKey()
final bool isUpdating;
@override
@JsonKey(name: "vehicle-type")
final String vehicleType;
@override
@@ -1876,7 +1827,7 @@ class _$ExternalProviderImpl implements _ExternalProvider {
@override
String toString() {
return 'ExternalProvider(name: $name, type: $type, path: $path, count: $count, isUpdating: $isUpdating, vehicleType: $vehicleType, updateAt: $updateAt)';
return 'ExternalProvider(name: $name, type: $type, vehicleType: $vehicleType, updateAt: $updateAt)';
}
@override
@@ -1886,10 +1837,6 @@ class _$ExternalProviderImpl implements _ExternalProvider {
other is _$ExternalProviderImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.path, path) || other.path == path) &&
(identical(other.count, count) || other.count == count) &&
(identical(other.isUpdating, isUpdating) ||
other.isUpdating == isUpdating) &&
(identical(other.vehicleType, vehicleType) ||
other.vehicleType == vehicleType) &&
(identical(other.updateAt, updateAt) ||
@@ -1898,8 +1845,8 @@ class _$ExternalProviderImpl implements _ExternalProvider {
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType, name, type, path, count, isUpdating, vehicleType, updateAt);
int get hashCode =>
Object.hash(runtimeType, name, type, vehicleType, updateAt);
@JsonKey(ignore: true)
@override
@@ -1920,9 +1867,6 @@ abstract class _ExternalProvider implements ExternalProvider {
const factory _ExternalProvider(
{required final String name,
required final String type,
required final String path,
required final int count,
final bool isUpdating,
@JsonKey(name: "vehicle-type") required final String vehicleType,
@JsonKey(name: "update-at") required final DateTime updateAt}) =
_$ExternalProviderImpl;
@@ -1935,12 +1879,6 @@ abstract class _ExternalProvider implements ExternalProvider {
@override
String get type;
@override
String get path;
@override
int get count;
@override
bool get isUpdating;
@override
@JsonKey(name: "vehicle-type")
String get vehicleType;
@override

View File

@@ -27,7 +27,7 @@ Map<String, dynamic> _$$ConfigExtendedParamsImplToJson(
_$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson(
Map<String, dynamic> json) =>
_$UpdateConfigParamsImpl(
profileId: json['profile-id'] as String,
profilePath: json['profile-path'] as String?,
config: ClashConfig.fromJson(json['config'] as Map<String, dynamic>),
params:
ConfigExtendedParams.fromJson(json['params'] as Map<String, dynamic>),
@@ -36,7 +36,7 @@ _$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson(
Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
_$UpdateConfigParamsImpl instance) =>
<String, dynamic>{
'profile-id': instance.profileId,
'profile-path': instance.profilePath,
'config': instance.config,
'params': instance.params,
};
@@ -156,9 +156,6 @@ _$ExternalProviderImpl _$$ExternalProviderImplFromJson(
_$ExternalProviderImpl(
name: json['name'] as String,
type: json['type'] as String,
path: json['path'] as String,
count: (json['count'] as num).toInt(),
isUpdating: json['isUpdating'] as bool? ?? false,
vehicleType: json['vehicle-type'] as String,
updateAt: DateTime.parse(json['update-at'] as String),
);
@@ -168,9 +165,6 @@ Map<String, dynamic> _$$ExternalProviderImplToJson(
<String, dynamic>{
'name': instance.name,
'type': instance.type,
'path': instance.path,
'count': instance.count,
'isUpdating': instance.isUpdating,
'vehicle-type': instance.vehicleType,
'update-at': instance.updateAt.toIso8601String(),
};

View File

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

View File

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

View File

@@ -223,8 +223,6 @@ mixin _$Profile {
bool get autoUpdate => throw _privateConstructorUsedError;
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
Set<String> get unfoldSet => throw _privateConstructorUsedError;
@JsonKey(includeToJson: false, includeFromJson: false)
bool get isUpdating => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -246,8 +244,7 @@ abstract class $ProfileCopyWith<$Res> {
UserInfo? userInfo,
bool autoUpdate,
Map<String, String> selectedMap,
Set<String> unfoldSet,
@JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating});
Set<String> unfoldSet});
$UserInfoCopyWith<$Res>? get userInfo;
}
@@ -275,7 +272,6 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile>
Object? autoUpdate = null,
Object? selectedMap = null,
Object? unfoldSet = null,
Object? isUpdating = null,
}) {
return _then(_value.copyWith(
id: null == id
@@ -318,10 +314,6 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile>
? _value.unfoldSet
: unfoldSet // ignore: cast_nullable_to_non_nullable
as Set<String>,
isUpdating: null == isUpdating
? _value.isUpdating
: isUpdating // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
@@ -355,8 +347,7 @@ abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> {
UserInfo? userInfo,
bool autoUpdate,
Map<String, String> selectedMap,
Set<String> unfoldSet,
@JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating});
Set<String> unfoldSet});
@override
$UserInfoCopyWith<$Res>? get userInfo;
@@ -383,7 +374,6 @@ class __$$ProfileImplCopyWithImpl<$Res>
Object? autoUpdate = null,
Object? selectedMap = null,
Object? unfoldSet = null,
Object? isUpdating = null,
}) {
return _then(_$ProfileImpl(
id: null == id
@@ -426,10 +416,6 @@ class __$$ProfileImplCopyWithImpl<$Res>
? _value._unfoldSet
: unfoldSet // ignore: cast_nullable_to_non_nullable
as Set<String>,
isUpdating: null == isUpdating
? _value.isUpdating
: isUpdating // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@@ -447,9 +433,7 @@ class _$ProfileImpl implements _Profile {
this.userInfo,
this.autoUpdate = true,
final Map<String, String> selectedMap = const {},
final Set<String> unfoldSet = const {},
@JsonKey(includeToJson: false, includeFromJson: false)
this.isUpdating = false})
final Set<String> unfoldSet = const {}})
: _selectedMap = selectedMap,
_unfoldSet = unfoldSet;
@@ -492,13 +476,9 @@ class _$ProfileImpl implements _Profile {
return EqualUnmodifiableSetView(_unfoldSet);
}
@override
@JsonKey(includeToJson: false, includeFromJson: false)
final bool isUpdating;
@override
String toString() {
return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet, isUpdating: $isUpdating)';
return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet)';
}
@override
@@ -522,9 +502,7 @@ class _$ProfileImpl implements _Profile {
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap) &&
const DeepCollectionEquality()
.equals(other._unfoldSet, _unfoldSet) &&
(identical(other.isUpdating, isUpdating) ||
other.isUpdating == isUpdating));
.equals(other._unfoldSet, _unfoldSet));
}
@JsonKey(ignore: true)
@@ -540,8 +518,7 @@ class _$ProfileImpl implements _Profile {
userInfo,
autoUpdate,
const DeepCollectionEquality().hash(_selectedMap),
const DeepCollectionEquality().hash(_unfoldSet),
isUpdating);
const DeepCollectionEquality().hash(_unfoldSet));
@JsonKey(ignore: true)
@override
@@ -568,9 +545,7 @@ abstract class _Profile implements Profile {
final UserInfo? userInfo,
final bool autoUpdate,
final Map<String, String> selectedMap,
final Set<String> unfoldSet,
@JsonKey(includeToJson: false, includeFromJson: false)
final bool isUpdating}) = _$ProfileImpl;
final Set<String> unfoldSet}) = _$ProfileImpl;
factory _Profile.fromJson(Map<String, dynamic> json) = _$ProfileImpl.fromJson;
@@ -595,9 +570,6 @@ abstract class _Profile implements Profile {
@override
Set<String> get unfoldSet;
@override
@JsonKey(includeToJson: false, includeFromJson: false)
bool get isUpdating;
@override
@JsonKey(ignore: true)
_$$ProfileImplCopyWith<_$ProfileImpl> get copyWith =>
throw _privateConstructorUsedError;

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,4 +1,3 @@
// ignore_for_file: invalid_annotation_target
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
@@ -56,9 +55,6 @@ class Profile with _$Profile {
@Default(true) bool autoUpdate,
@Default({}) SelectedMap selectedMap,
@Default({}) Set<String> unfoldSet,
@JsonKey(includeToJson: false, includeFromJson: false)
@Default(false)
bool isUpdating,
}) = _Profile;
factory Profile.fromJson(Map<String, Object?> json) =>
@@ -67,7 +63,7 @@ class Profile with _$Profile {
factory Profile.normal({
String? label,
String url = '',
}) {
}) {
return Profile(
label: label,
url: url,
@@ -81,7 +77,8 @@ extension ProfileExtension on Profile {
ProfileType get type =>
url.isEmpty == true ? ProfileType.file : ProfileType.url;
bool get realAutoUpdate => url.isEmpty == true ? false : autoUpdate;
bool get realAutoUpdate =>
url.isEmpty == true ? false : autoUpdate;
Future<void> checkAndUpdate() async {
final isExists = await check();

View File

@@ -41,4 +41,4 @@ class Proxy with _$Proxy {
}) = _Proxy;
factory Proxy.fromJson(Map<String, Object?> json) => _$ProxyFromJson(json);
}
}

View File

@@ -1,10 +1,7 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lpinyin/lpinyin.dart';
part 'generated/selector.freezed.dart';
@@ -37,18 +34,10 @@ class ProfilesSelectorState with _$ProfilesSelectorState {
const factory ProfilesSelectorState({
required List<Profile> profiles,
required String? currentProfileId,
required int columns,
required ViewMode viewMode,
}) = _ProfilesSelectorState;
}
@freezed
class NetworkDetectionState with _$NetworkDetectionState {
const factory NetworkDetectionState({
required bool isTesting,
required IpInfo? ipInfo,
}) = _NetworkDetectionState;
}
@freezed
class ApplicationSelectorState with _$ApplicationSelectorState {
const factory ApplicationSelectorState({
@@ -64,9 +53,7 @@ class TrayContainerSelectorState with _$TrayContainerSelectorState {
const factory TrayContainerSelectorState({
required Mode mode,
required bool autoLaunch,
required bool systemProxy,
required bool tunEnable,
required bool isStart,
required bool isRun,
required String? locale,
}) = _TrayContainerSelectorState;
}
@@ -145,41 +132,18 @@ class MoreToolsSelectorState with _$MoreToolsSelectorState {
@freezed
class PackageListSelectorState with _$PackageListSelectorState {
const factory PackageListSelectorState({
required List<Package> packages,
required AccessControl accessControl,
required bool isAccessControl,
}) = _PackageListSelectorState;
}
extension PackageListSelectorStateExt on PackageListSelectorState {
List<Package> getList(List<String> selectedList) {
final isFilterSystemApp = accessControl.isFilterSystemApp;
final sort = accessControl.sort;
return packages
.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
class ColumnsSelectorState with _$ColumnsSelectorState {
const factory ColumnsSelectorState({
required int columns,
required ViewMode viewMode,
}) = _ColumnsSelectorState;
}
@freezed
@@ -191,48 +155,9 @@ class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState {
}
@freezed
class ProxiesActionsState with _$ProxiesActionsState {
const factory ProxiesActionsState({
required bool isCurrent,
required bool hasProvider,
}) = _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;
}
class CurrentGroupProxyNameSelectorState with _$CurrentGroupProxyNameSelectorState {
const factory CurrentGroupProxyNameSelectorState({
required String? proxyName,
required String? proxyName2,
}) = _CurrentGroupProxyNameSelectorState;
}

View File

@@ -13,6 +13,20 @@ typedef OnSelected = void Function(int index);
class HomePage extends StatelessWidget {
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({
required BuildContext context,
required ViewMode viewMode,
@@ -33,78 +47,62 @@ class HomePage extends StatelessWidget {
selectedIndex: currentIndex,
);
}
return LayoutBuilder(
builder: (_, container) {
return Material(
color: context.colorScheme.surfaceContainer,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 16,
),
height: container.maxHeight,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: IntrinsicHeight(
child: Selector<Config, bool>(
selector: (_, config) => config.showLabel,
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,
);
},
),
),
),
final extended = viewMode == ViewMode.desktop;
return _navigationBarContainer(
context: context,
child: NavigationRail(
groupAlignment: -0.8,
selectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
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),
),
const SizedBox(
height: 16,
),
IconButton(
onPressed: () {
final config = globalState.appController.config;
config.showLabel = !config.showLabel;
},
icon: const Icon(Icons.menu),
)
],
),
)
.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),
),
),
),
);
},
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
extended: extended,
minExtendedWidth: 172,
selectedIndex: currentIndex,
labelType: extended
? NavigationRailLabelType.none
: NavigationRailLabelType.selected,
);
}

View File

@@ -48,16 +48,6 @@ 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 {
return await methodChannel.invokeMethod<bool>("openFile", {
"path": path,

View File

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

View File

@@ -1,29 +0,0 @@
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

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

View File

@@ -1,6 +1,4 @@
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ScrollOverBuilder extends StatefulWidget {
final Widget Function(bool isOver) builder;
@@ -17,6 +15,7 @@ class ScrollOverBuilder extends StatefulWidget {
class _ScrollOverBuilderState extends State<ScrollOverBuilder> {
final isOverNotifier = ValueNotifier<bool>(false);
@override
void dispose() {
super.dispose();
@@ -39,29 +38,3 @@ class _ScrollOverBuilderState extends State<ScrollOverBuilder> {
);
}
}
class ProxiesActionsBuilder extends StatelessWidget {
final Widget? child;
final Widget Function(
ProxiesActionsState state,
Widget? child,
) builder;
const ProxiesActionsBuilder({
super.key,
required this.child,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Selector<AppState, ProxiesActionsState>(
selector: (_, appState) => ProxiesActionsState(
isCurrent: appState.currentLabel == "proxies",
hasProvider: appState.providers.isNotEmpty,
),
builder: (_, state, child) => builder(state, child),
child: child,
);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'text.dart';
@@ -30,47 +29,43 @@ class InfoHeader extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
if (info.iconData != null) ...[
Icon(
info.iconData,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(
width: 8,
),
],
Flexible(
flex: 1,
child: TooltipText(
text: Text(
info.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
),
],
),
),
const SizedBox(
width: 8,
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
...actions,
if (info.iconData != null) ...[
Icon(
info.iconData,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(
width: 8,
),
],
Flexible(
child: TooltipText(
text: Text(
info.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
),
],
),
Expanded(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
...actions,
],
),
),
],
),
);
@@ -151,28 +146,18 @@ class CommonCard extends StatelessWidget {
childWidget = Column(
mainAxisSize: MainAxisSize.min,
children: [
InfoHeader(
info: info!,
Flexible(
flex: 0,
child: InfoHeader(
info: info!,
),
),
Flexible(
flex: 1,
child: child,
),
],
);
}
if (selectWidget != null && isSelected) {
final List<Widget> children = [];
children.add(childWidget);
children.add(
Positioned.fill(
child: selectWidget!,
),
);
childWidget = Stack(
children: children,
);
}
return OutlinedButton(
clipBehavior: Clip.antiAlias,
style: ButtonStyle(
@@ -190,7 +175,25 @@ class CommonCard extends StatelessWidget {
),
),
onPressed: onPressed,
child: childWidget,
child: Builder(
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,12 +1,10 @@
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/plugins/proxy.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../common/function.dart';
class ClashContainer extends StatefulWidget {
final Widget child;
@@ -21,53 +19,14 @@ class ClashContainer extends StatefulWidget {
class _ClashContainerState extends State<ClashContainer>
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) {
return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState(
accessControl: config.isAccessControl ? config.accessControl : null,
enable: config.vpnProps.enable,
allowBypass: config.vpnProps.allowBypass,
systemProxy: config.vpnProps.systemProxy,
allowBypass: config.allowBypass,
systemProxy: config.systemProxy,
mixedPort: clashConfig.mixedPort,
onlyProxy: config.onlyProxy,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
),
builder: (__, state, child) {
clashCore.setState(state);
@@ -77,38 +36,9 @@ class _ClashContainerState extends State<ClashContainer>
);
}
_changeProfile() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (globalState.autoRun) {
globalState.autoRun = false;
return;
}
final appController = globalState.appController;
appController.appState.delayMap = {};
await appController.applyProfile();
});
}
Widget _changeProfileContainer(Widget child) {
return Selector<Config, String?>(
selector: (_, config) => config.currentProfileId,
builder: (__, state, child) {
_changeProfile();
return child!;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return _changeProfileContainer(
_updateCoreState(
_updateContainer(
widget.child,
),
),
);
return _updateCoreState(widget.child);
}
@override
@@ -124,20 +54,15 @@ class _ClashContainerState extends State<ClashContainer>
}
@override
Future<void> onDelay(Delay delay) async {
void onDelay(Delay delay) {
final appController = globalState.appController;
appController.setDelay(delay);
super.onDelay(delay);
await globalState.appController.updateGroupDebounce();
}
@override
void onLog(Log log) {
globalState.appController.appState.addLog(log);
if (log.logLevel == LogLevel.error) {
globalState.appController.showSnackBar(log.payload ?? '');
}
debugPrint("$log");
super.onLog(log);
}
@@ -148,14 +73,24 @@ class _ClashContainerState extends State<ClashContainer>
}
@override
void onLoaded(String providerName) {
void onLoaded(String groupName) {
final appController = globalState.appController;
appController.appState.setProvider(
clashCore.getExternalProvider(
providerName,
),
final currentSelectedMap = appController.config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
if (proxyName == null) return;
appController.changeProxy(
groupName: groupName,
proxyName: proxyName,
);
super.onLoaded(proxyName);
}
@override
Future<void> onStarted(String runTime) async {
super.onStarted(runTime);
proxy?.updateStartTime();
final appController = globalState.appController;
await appController.applyProfile(isPrue: true);
appController.addCheckIpNumDebounce();
super.onLoaded(providerName);
}
}

View File

@@ -1,37 +0,0 @@
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,
builder: (_, actions, __) {
final realActions =
actions.isNotEmpty ? actions : widget.actions;
actions.isNotEmpty ? actions : widget.actions;
return AppBar(
centerTitle: false,
automaticallyImplyLeading: widget.automaticallyImplyLeading,

View File

@@ -1,74 +0,0 @@
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

@@ -5,12 +5,10 @@ import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'side_sheet.dart';
showExtendPage(
BuildContext context, {
showExtendPage(BuildContext context, {
required Widget body,
required String title,
double? extendPageWidth,
bool forceNotSide = false,
Widget? action,
}) {
final NavigatorState navigator = Navigator.of(context);
@@ -19,24 +17,23 @@ showExtendPage(
key: globalKey,
child: body,
);
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
final isNotSide = isMobile || forceNotSide;
final isMobile = globalState.appController.appState.viewMode ==
ViewMode.mobile;
navigator.push(
ModalSideSheetRoute(
modalBarrierColor: Colors.black38,
builder: (context) {
final commonScaffold = CommonScaffold(
automaticallyImplyLeading: isNotSide,
actions: isNotSide
automaticallyImplyLeading: isMobile ? true : false,
actions: isMobile
? null
: [
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
),
],
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
),
],
title: title,
body: uniqueBody,
);

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