Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
402221aaa2 | ||
|
|
f6d9ed11d9 | ||
|
|
c38a671d57 | ||
|
|
75af47aead | ||
|
|
8dafe3b0ec | ||
|
|
813198a21d | ||
|
|
68dd262fef | ||
|
|
5ef020db73 | ||
|
|
e3c9035903 | ||
|
|
7fc54c5295 | ||
|
|
00a78b5fb4 | ||
|
|
8cdaf30de0 | ||
|
|
f39b9cf933 | ||
|
|
9df1ff46c2 | ||
|
|
fcbbbdc698 | ||
|
|
3ba8355772 | ||
|
|
f6b97f82ae | ||
|
|
13ac20f273 | ||
|
|
6de89d7de4 | ||
|
|
c36df8cb4a |
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -87,7 +87,7 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.x'
|
||||
flutter-version: 3.22.x
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
@@ -136,8 +136,29 @@ 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'
|
||||
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/
|
||||
destination-github-username: chen08209
|
||||
destination-repository-name: FlClash-fdroid-repo
|
||||
user-name: 'github-actions[bot]'
|
||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||
target-branch: action-pr
|
||||
commit-message: Update from ${{ github.ref_name }}
|
||||
target-directory: /tmp/
|
||||
|
||||
@@ -38,6 +38,10 @@ on Mobile:
|
||||
|
||||
✨ Support subscription link, Dark mode
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
||||
|
||||
## Contact
|
||||
|
||||
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
|
||||
|
||||
@@ -38,6 +38,11 @@ on Mobile:
|
||||
|
||||
✨ 支持一键导入订阅, 深色模式
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
|
||||
|
||||
@@ -102,6 +102,9 @@ 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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,18 +14,20 @@
|
||||
<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:icon="@mipmap/ic_launcher"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:extractNativeLibs="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name="com.follow.clash.MainActivity"
|
||||
@@ -56,17 +58,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"
|
||||
@@ -75,8 +77,8 @@
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:label="FlClash"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
@@ -86,6 +88,29 @@
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name=".FilesProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS"
|
||||
android:process=":background">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashVpnService"
|
||||
android:exported="false"
|
||||
@@ -96,6 +121,11 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse" />
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.follow.clash
|
||||
|
||||
import com.follow.clash.models.Props
|
||||
|
||||
interface BaseServiceInterface {
|
||||
fun start(port: Int, props: Props?): Int?
|
||||
fun stop()
|
||||
fun startForeground(title: String, content: String)
|
||||
}
|
||||
112
android/app/src/main/kotlin/com/follow/clash/FilesProvider.kt
Normal file
112
android/app/src/main/kotlin/com/follow/clash/FilesProvider.kt
Normal file
@@ -0,0 +1,112 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.os.CancellationSignal
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract.Document
|
||||
import android.provider.DocumentsContract.Root
|
||||
import android.provider.DocumentsProvider
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
class FilesProvider : DocumentsProvider() {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_ROOT_ID = "0"
|
||||
|
||||
private val DEFAULT_DOCUMENT_COLUMNS = arrayOf(
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.COLUMN_SIZE,
|
||||
)
|
||||
private val DEFAULT_ROOT_COLUMNS = arrayOf(
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_ICON,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_SUMMARY,
|
||||
Root.COLUMN_DOCUMENT_ID
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun queryRoots(projection: Array<String>?): Cursor {
|
||||
return MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply {
|
||||
newRow().apply {
|
||||
add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
|
||||
add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY)
|
||||
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
||||
add(Root.COLUMN_TITLE, context!!.getString(R.string.fl_clash))
|
||||
add(Root.COLUMN_SUMMARY, "Data")
|
||||
add(Root.COLUMN_DOCUMENT_ID, "/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun queryChildDocuments(
|
||||
parentDocumentId: String,
|
||||
projection: Array<String>?,
|
||||
sortOrder: String?
|
||||
): Cursor {
|
||||
val result = MatrixCursor(resolveDocumentProjection(projection))
|
||||
val parentFile = if (parentDocumentId == "/") {
|
||||
context?.filesDir
|
||||
} else {
|
||||
File(parentDocumentId)
|
||||
} ?: throw FileNotFoundException("Parent directory not found")
|
||||
parentFile.listFiles()?.forEach { file ->
|
||||
includeFile(result, file)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
|
||||
val result = MatrixCursor(resolveDocumentProjection(projection))
|
||||
val file = File(documentId)
|
||||
includeFile(result, file)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun openDocument(
|
||||
documentId: String,
|
||||
mode: String,
|
||||
signal: CancellationSignal?
|
||||
): ParcelFileDescriptor {
|
||||
val file = File(documentId)
|
||||
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
||||
return ParcelFileDescriptor.open(file, accessMode)
|
||||
}
|
||||
|
||||
private fun includeFile(result: MatrixCursor, file: File) {
|
||||
result.newRow().apply {
|
||||
add(Document.COLUMN_DOCUMENT_ID, file.absolutePath)
|
||||
add(Document.COLUMN_DISPLAY_NAME, file.name)
|
||||
add(Document.COLUMN_SIZE, file.length())
|
||||
add(
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.FLAG_SUPPORTS_WRITE or Document.FLAG_SUPPORTS_DELETE
|
||||
)
|
||||
add(Document.COLUMN_MIME_TYPE, getDocumentType(file))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDocumentType(file: File): String {
|
||||
return if (file.isDirectory) {
|
||||
Document.MIME_TYPE_DIR
|
||||
} else {
|
||||
"application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDocumentProjection(projection: Array<String>?): Array<String> {
|
||||
return projection ?: DEFAULT_DOCUMENT_COLUMNS
|
||||
}
|
||||
}
|
||||
@@ -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.ProxyPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import io.flutter.FlutterInjector
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
@@ -22,6 +22,7 @@ enum class RunState {
|
||||
object GlobalState {
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
val runLock = ReentrantLock()
|
||||
|
||||
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
@@ -37,6 +38,11 @@ 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
|
||||
@@ -47,9 +53,10 @@ object GlobalState {
|
||||
lock.withLock {
|
||||
destroyServiceEngine()
|
||||
serviceEngine = FlutterEngine(context)
|
||||
serviceEngine?.plugins?.add(ProxyPlugin())
|
||||
serviceEngine?.plugins?.add(VpnPlugin())
|
||||
serviceEngine?.plugins?.add(AppPlugin())
|
||||
serviceEngine?.plugins?.add(TilePlugin())
|
||||
serviceEngine?.plugins?.add(ServicePlugin())
|
||||
val vpnService = DartExecutor.DartEntrypoint(
|
||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
|
||||
"vpnService"
|
||||
@@ -57,8 +64,6 @@ object GlobalState {
|
||||
serviceEngine?.dartExecutor?.executeDartEntrypoint(
|
||||
vpnService,
|
||||
)
|
||||
|
||||
Log.e("FlClashVpnService", "initServiceEngine ===>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.follow.clash
|
||||
|
||||
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ProxyPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
@@ -12,7 +13,8 @@ class MainActivity : FlutterActivity() {
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(AppPlugin())
|
||||
flutterEngine.plugins.add(ProxyPlugin())
|
||||
flutterEngine.plugins.add(VpnPlugin())
|
||||
flutterEngine.plugins.add(ServicePlugin())
|
||||
flutterEngine.plugins.add(TilePlugin())
|
||||
GlobalState.flutterEngine = flutterEngine
|
||||
}
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
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 java.net.URL
|
||||
import androidx.core.app.NotificationCompat
|
||||
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 {
|
||||
@@ -31,7 +41,6 @@ fun Metadata.getProtocol(): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun String.getInetSocketAddress(): InetSocketAddress {
|
||||
val url = URL("https://$this")
|
||||
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
|
||||
}
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
import java.util.Date
|
||||
|
||||
data class Package(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val isSystem:Boolean
|
||||
val isSystem: Boolean,
|
||||
val firstInstallTime: Long,
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ data class AccessControl(
|
||||
)
|
||||
|
||||
data class Props(
|
||||
val enable: Boolean?,
|
||||
val accessControl: AccessControl?,
|
||||
val allowBypass: Boolean?,
|
||||
val systemProxy: Boolean?,
|
||||
|
||||
@@ -4,12 +4,19 @@ import android.Manifest
|
||||
import android.app.Activity
|
||||
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
|
||||
import com.follow.clash.extensions.getBase64
|
||||
@@ -17,6 +24,7 @@ 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
|
||||
@@ -28,8 +36,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
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 {
|
||||
|
||||
@@ -37,7 +46,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
private var toast: Toast? = null
|
||||
|
||||
private var context: Context? = null
|
||||
private lateinit var context: Context
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
@@ -45,14 +54,78 @@ 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) {
|
||||
@@ -61,7 +134,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private fun tip(message: String?) {
|
||||
if(GlobalState.flutterEngine == null){
|
||||
if (GlobalState.flutterEngine == null) {
|
||||
if (toast != null) {
|
||||
toast!!.cancel()
|
||||
}
|
||||
@@ -85,7 +158,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
"getPackages" -> {
|
||||
scope.launch {
|
||||
result.success(getPackages())
|
||||
result.success(getPackagesToJson())
|
||||
}
|
||||
}
|
||||
|
||||
"getChinaPackageNames" -> {
|
||||
scope.launch {
|
||||
result.success(getChinaPackageNames())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +183,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
|
||||
@@ -131,12 +210,8 @@ 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(
|
||||
@@ -152,7 +227,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())
|
||||
}
|
||||
}
|
||||
@@ -164,15 +239,56 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"openFile" -> {
|
||||
val path = call.argument<String>("path")!!
|
||||
openFile(path)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
activity?.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
println(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExcludeFromRecents(value: Boolean?) {
|
||||
if (context == null) return
|
||||
val am = getSystemService(context!!, ActivityManager::class.java)
|
||||
val am = getSystemService(context, ActivityManager::class.java)
|
||||
val task = am?.appTasks?.firstOrNull {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.taskInfo.taskId == activity?.taskId
|
||||
@@ -189,7 +305,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()
|
||||
@@ -201,32 +317,144 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
return iconMap[packageName]
|
||||
}
|
||||
|
||||
private suspend fun getPackages(): String {
|
||||
return withContext(Dispatchers.Default) {
|
||||
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 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
|
||||
)
|
||||
}
|
||||
}?.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 {
|
||||
return withContext(Dispatchers.Default) {
|
||||
Gson().toJson(getPackages())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getChinaPackageNames(): String {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val packages: List<String> =
|
||||
getPackages().map { it.packageName }.filter { isChinaPackage(it) }
|
||||
Gson().toJson(packages)
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -241,4 +469,25 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.services.FlClashVpnService
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
|
||||
val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var context: Context? = null
|
||||
private var flClashVpnService: FlClashVpnService? = null
|
||||
private var port: Int = 7890
|
||||
private var props: Props? = null
|
||||
private var isBlockNotification: Boolean = false
|
||||
private var isStart: Boolean = false
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
val binder = service as FlClashVpnService.LocalBinder
|
||||
flClashVpnService = binder.getService()
|
||||
if (isStart) {
|
||||
startVpn()
|
||||
} else {
|
||||
flClashVpnService?.initServiceEngine()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
flClashVpnService = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "proxy")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"initService" -> {
|
||||
isStart = false
|
||||
initService()
|
||||
requestNotificationsPermission()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"startProxy" -> {
|
||||
isStart = true
|
||||
port = call.argument<Int>("port")!!
|
||||
val args = call.argument<String>("args")
|
||||
props =
|
||||
if (args != null) Gson().fromJson(args, Props::class.java) else null
|
||||
startVpn()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"stopProxy" -> {
|
||||
stopVpn()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("fd")
|
||||
if (fd != null) {
|
||||
flClashVpnService?.protect(fd)
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
"startForeground" -> {
|
||||
val title = call.argument<String>("title") as String
|
||||
val content = call.argument<String>("content") as String
|
||||
startForeground(title, content)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initService() {
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
if (flClashVpnService != null) {
|
||||
flClashVpnService!!.initServiceEngine()
|
||||
} else {
|
||||
bindService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startVpn() {
|
||||
if (flClashVpnService == null) {
|
||||
bindService()
|
||||
return
|
||||
}
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
stopVpn()
|
||||
return
|
||||
}
|
||||
val fd = flClashVpnService?.start(port, props)
|
||||
flutterMethodChannel.invokeMethod("started", fd)
|
||||
}
|
||||
|
||||
private fun stopVpn() {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
flClashVpnService?.stop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
private fun startForeground(title: String, content: String) {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
flClashVpnService?.startForeground(title, content)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||
}
|
||||
|
||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == FlutterActivity.RESULT_OK) {
|
||||
bindService()
|
||||
} else {
|
||||
stopVpn()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onRequestPermissionsResultListener(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
isBlockNotification = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun requestNotificationsPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = context?.let {
|
||||
ContextCompat.checkSelfPermission(
|
||||
it,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
}
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
if (isBlockNotification) return
|
||||
if (activity == null) return
|
||||
ActivityCompat.requestPermissions(
|
||||
activity!!,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = Intent(context, FlClashVpnService::class.java)
|
||||
context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.content.Context
|
||||
import com.follow.clash.GlobalState
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"init" -> {
|
||||
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
|
||||
GlobalState.initServiceEngine(context)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"destroy" -> {
|
||||
handleDestroy()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDestroy() {
|
||||
GlobalState.getCurrentVPNPlugin()?.stop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.services.FlClashService
|
||||
import com.follow.clash.services.FlClashVpnService
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
|
||||
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
private lateinit var context: Context
|
||||
private var flClashService: BaseServiceInterface? = null
|
||||
private var port: Int = 7890
|
||||
private var props: Props? = null
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
flClashService = when (service) {
|
||||
is FlClashVpnService.LocalBinder -> service.getService()
|
||||
is FlClashService.LocalBinder -> service.getService()
|
||||
else -> throw Exception("invalid binder")
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
flClashService = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
|
||||
"start" -> {
|
||||
port = call.argument<Int>("port")!!
|
||||
val args = call.argument<String>("args")
|
||||
props =
|
||||
if (args != null) Gson().fromJson(args, Props::class.java) else null
|
||||
when (props?.enable == true) {
|
||||
true -> handleStartVpn()
|
||||
false -> start()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"stop" -> {
|
||||
stop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("fd")
|
||||
if (fd != null) {
|
||||
if (flClashService is FlClashVpnService) {
|
||||
(flClashService as FlClashVpnService).protect(fd)
|
||||
}
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
"startForeground" -> {
|
||||
val title = call.argument<String>("title") as String
|
||||
val content = call.argument<String>("content") as String
|
||||
startForeground(title, content)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
private fun startForeground(title: String, content: String) {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
flClashService?.startForeground(title, content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
if (flClashService == null) {
|
||||
bindService()
|
||||
return
|
||||
}
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val fd = flClashService?.start(port, props)
|
||||
flutterMethodChannel.invokeMethod("started", fd)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
flClashService?.stop()
|
||||
}
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = when (props?.enable == true) {
|
||||
true -> Intent(context, FlClashVpnService::class.java)
|
||||
false -> Intent(context, FlClashService::class.java)
|
||||
}
|
||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.models.Props
|
||||
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): FlClashService = this@FlClashService
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start(port: Int, props: Props?): Int? = null
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
@@ -15,6 +16,7 @@ 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
|
||||
@@ -25,10 +27,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class FlClashVpnService : VpnService() {
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
@SuppressLint("WrongConstant")
|
||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
|
||||
private val passList = listOf(
|
||||
"*zhihu.com",
|
||||
@@ -52,10 +52,10 @@ class FlClashVpnService : VpnService() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initServiceEngine()
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
}
|
||||
|
||||
fun start(port: Int, props: Props?): Int? {
|
||||
override fun start(port: Int, props: Props?): Int? {
|
||||
return with(Builder()) {
|
||||
addAddress("172.16.0.1", 30)
|
||||
setMtu(9000)
|
||||
@@ -97,11 +97,18 @@ class FlClashVpnService : VpnService() {
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
stopForeground()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
||||
@@ -136,16 +143,8 @@ class FlClashVpnService : VpnService() {
|
||||
}
|
||||
}
|
||||
|
||||
fun initServiceEngine() {
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentAppPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
fun startForeground(title: String, content: String) {
|
||||
@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)
|
||||
@@ -157,17 +156,16 @@ class FlClashVpnService : VpnService() {
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForeground() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentAppPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
@@ -190,7 +188,6 @@ class FlClashVpnService : VpnService() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
6
android/app/src/main/res/xml/file_paths.xml
Normal file
6
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<paths>
|
||||
<files-path
|
||||
name="files"
|
||||
path="."/>
|
||||
</paths>
|
||||
|
||||
BIN
assets/fonts/Twemoji.Mozilla.ttf
Normal file
BIN
assets/fonts/Twemoji.Mozilla.ttf
Normal file
Binary file not shown.
Submodule core/Clash.Meta updated: 0292a65f16...0125a90a77
268
core/common.go
268
core/common.go
@@ -2,20 +2,9 @@ package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"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"
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -24,43 +13,27 @@ 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"`
|
||||
@@ -69,9 +42,9 @@ type ConfigExtendedParams struct {
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
ProfilePath *string `json:"profile-path"`
|
||||
Config config.RawConfig `json:"config" `
|
||||
Params ConfigExtendedParams `json:"params"`
|
||||
ProfileId string `json:"profile-id"`
|
||||
Config config.RawConfig `json:"config" `
|
||||
Params ConfigExtendedParams `json:"params"`
|
||||
}
|
||||
|
||||
type ChangeProxyParams struct {
|
||||
@@ -93,9 +66,19 @@ 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()
|
||||
@@ -145,26 +128,108 @@ func removeFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRawConfigWithPath(path *string) *config.RawConfig {
|
||||
if path == 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")
|
||||
return config.DefaultRawConfig()
|
||||
} else {
|
||||
bytes, err := readFile(*path)
|
||||
if err != nil {
|
||||
log.Errorln("getProfile readFile error %v", err)
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
for _, mapping := range prof.RuleProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
return prof
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig {
|
||||
prof := getRawConfigWithPath(profilePath)
|
||||
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)
|
||||
overwriteConfig(prof, cfg)
|
||||
return prof
|
||||
}
|
||||
@@ -327,6 +392,7 @@ 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
|
||||
@@ -357,30 +423,65 @@ 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)
|
||||
tunnel.SetMode(general.Mode)
|
||||
log.SetLevel(general.LogLevel)
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
resolver.DisableIPv6 = !general.IPv6
|
||||
}
|
||||
|
||||
func patchSelectGroup() {
|
||||
@@ -408,12 +509,8 @@ func patchSelectGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
var applyLock sync.Mutex
|
||||
|
||||
func applyConfig() {
|
||||
applyLock.Lock()
|
||||
defer applyLock.Unlock()
|
||||
cfg, err := config.ParseRawConfig(currentConfig)
|
||||
func applyConfig() error {
|
||||
cfg, err := config.ParseRawConfig(currentRawConfig)
|
||||
if err != nil {
|
||||
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||
}
|
||||
@@ -423,8 +520,15 @@ func applyConfig() {
|
||||
if configParams.IsPatch {
|
||||
patchConfig(cfg.General)
|
||||
} else {
|
||||
closeConnections()
|
||||
runtime.GC()
|
||||
hub.UltraApplyConfig(cfg, true)
|
||||
hub.UltraApplyConfig(cfg)
|
||||
patchSelectGroup()
|
||||
}
|
||||
if isRunning {
|
||||
updateListeners(cfg.General, cfg.Listeners)
|
||||
hcCompatibleProvider(cfg.Providers)
|
||||
}
|
||||
externalProviders = getExternalProvidersRaw()
|
||||
return err
|
||||
}
|
||||
|
||||
18
core/go.mod
18
core/go.mod
@@ -16,7 +16,6 @@ require (
|
||||
github.com/3andne/restls-client-go v0.1.6 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
@@ -46,22 +45,23 @@ require (
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/chacha v0.1.0 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.6 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.7 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.1 // indirect
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect
|
||||
github.com/metacubex/utls v1.6.6 // indirect
|
||||
@@ -76,9 +76,10 @@ require (
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
github.com/sagernet/sing v0.5.0-alpha.10 // indirect
|
||||
github.com/sagernet/sing v0.5.0-alpha.13 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
@@ -96,13 +97,14 @@ require (
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
|
||||
36
core/go.sum
36
core/go.sum
@@ -7,8 +7,6 @@ github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4
|
||||
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
@@ -90,8 +88,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -108,6 +106,8 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
|
||||
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
|
||||
@@ -118,14 +118,14 @@ github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiL
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e h1:o+zohxPRo45P35fS9u1zfdBgr+L/7S0ObGU6YjbVBIc=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e/go.mod h1:WwJGbCx7bQcBzuQXiDOJvZH27R0kIjKNNlISIWsL6kM=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.7 h1:9f3Dt2+71TNp0e202llA2ug5h/rkWs2EZxQ5IMpf+9g=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.7/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.1 h1:XIZBXlazp8EEoPp1S0DViAhLkJakjQ2f+AOwwdKKNYg=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.1/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d h1:iYlepjRCYlPXtELupDL+pQjGqkCnQz4KQOfKImP9sog=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c=
|
||||
@@ -166,13 +166,15 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
|
||||
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
|
||||
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.5.0-alpha.10 h1:kuHl10gpjbKQAdQfyogQU3u0CVnpqC3wrAHe/+BFaXc=
|
||||
github.com/sagernet/sing v0.5.0-alpha.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.5.0-alpha.13 h1:fpR4TFZfu/9V3LbHSAnnnwcaXGMF8ijmAAPoY2WHSKw=
|
||||
github.com/sagernet/sing v0.5.0-alpha.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
|
||||
@@ -218,6 +220,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
@@ -257,8 +261,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
291
core/hub.go
291
core/hub.go
@@ -8,35 +8,50 @@ 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/geodata"
|
||||
"github.com/metacubex/mihomo/component/mmdb"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
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 currentConfig = config.DefaultRawConfig()
|
||||
var currentRawConfig = config.DefaultRawConfig()
|
||||
|
||||
var configParams = ConfigExtendedParams{}
|
||||
|
||||
var externalProviders = map[string]cp.Provider{}
|
||||
|
||||
var isInit = false
|
||||
|
||||
var currentProfileName = ""
|
||||
//export start
|
||||
func start() {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
isRunning = true
|
||||
}
|
||||
|
||||
//export stop
|
||||
func stop() {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
isRunning = false
|
||||
stopListeners()
|
||||
}
|
||||
|
||||
//export initClash
|
||||
func initClash(homeDirStr *C.char) bool {
|
||||
@@ -61,10 +76,10 @@ func restartClash() bool {
|
||||
|
||||
//export shutdownClash
|
||||
func shutdownClash() bool {
|
||||
stopListeners()
|
||||
executor.Shutdown()
|
||||
runtime.GC()
|
||||
isInit = false
|
||||
currentConfig = nil
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -76,16 +91,6 @@ 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)
|
||||
@@ -100,11 +105,15 @@ 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 {
|
||||
@@ -112,43 +121,23 @@ func updateConfig(s *C.char, port C.longlong) {
|
||||
return
|
||||
}
|
||||
configParams = params.Params
|
||||
prof := decorationConfig(params.ProfilePath, params.Config)
|
||||
currentConfig = prof
|
||||
applyConfig()
|
||||
prof := decorationConfig(params.ProfileId, params.Config)
|
||||
currentRawConfig = prof
|
||||
err = applyConfig()
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export clearEffect
|
||||
func clearEffect(s *C.char) {
|
||||
path := C.GoString(s)
|
||||
id := C.GoString(s)
|
||||
go func() {
|
||||
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)
|
||||
_ = removeFile(getProfilePath(id))
|
||||
_ = removeFile(getProfileProvidersPath(id))
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -164,35 +153,36 @@ func getProxies() *C.char {
|
||||
//export changeProxy
|
||||
func changeProxy(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
var params = &ChangeProxyParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
log.Infoln("Unmarshal ChangeProxyParams %v", err)
|
||||
}
|
||||
groupName := *params.GroupName
|
||||
proxyName := *params.ProxyName
|
||||
proxies := tunnel.ProxiesWithProviders()
|
||||
group, ok := proxies[groupName]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
adapterProxy := group.(*adapter.Proxy)
|
||||
selector, ok := adapterProxy.ProxyAdapter.(*outboundgroup.Selector)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var params = &ChangeProxyParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
log.Infoln("Unmarshal ChangeProxyParams %v", err)
|
||||
}
|
||||
groupName := *params.GroupName
|
||||
proxyName := *params.ProxyName
|
||||
proxies := tunnel.ProxiesWithProviders()
|
||||
group, ok := proxies[groupName]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
adapterProxy := group.(*adapter.Proxy)
|
||||
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if proxyName == "" {
|
||||
selector.ForceSet(proxyName)
|
||||
} else {
|
||||
err = selector.Set(proxyName)
|
||||
if err == nil {
|
||||
log.Infoln("[Selector] %s selected %s", groupName, proxyName)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if err == nil {
|
||||
log.Infoln("[SelectAble] %s selected %s", groupName, proxyName)
|
||||
}
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic() *C.char {
|
||||
up, down := statistic.DefaultManager.Now()
|
||||
up, down := statistic.DefaultManager.Current(state.OnlyProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -207,7 +197,7 @@ func getTraffic() *C.char {
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic() *C.char {
|
||||
up, down := statistic.DefaultManager.Total()
|
||||
up, down := statistic.DefaultManager.Total(state.OnlyProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -229,16 +219,16 @@ func resetTraffic() {
|
||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
b.Go(paramsString, func() (bool, error) {
|
||||
var params = &TestDelayParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
return
|
||||
return false, nil
|
||||
}
|
||||
|
||||
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
|
||||
if err != nil {
|
||||
return
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
|
||||
@@ -255,7 +245,7 @@ func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
return false, nil
|
||||
}
|
||||
|
||||
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
|
||||
@@ -263,14 +253,14 @@ func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
return false, nil
|
||||
}
|
||||
|
||||
delayData.Value = int32(delay)
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
}()
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
//export getVersionInfo
|
||||
@@ -299,7 +289,7 @@ func getConnections() *C.char {
|
||||
}
|
||||
|
||||
//export closeConnections
|
||||
func closeConnections() bool {
|
||||
func closeConnections() {
|
||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
@@ -307,17 +297,16 @@ func closeConnections() bool {
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
//export closeConnection
|
||||
func closeConnection(id *C.char) bool {
|
||||
func closeConnection(id *C.char) {
|
||||
connectionId := C.GoString(id)
|
||||
err := statistic.DefaultManager.Get(connectionId).Close()
|
||||
if err != nil {
|
||||
return false
|
||||
c := statistic.DefaultManager.Get(connectionId)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
return true
|
||||
_ = c.Close()
|
||||
}
|
||||
|
||||
//export getProviders
|
||||
@@ -345,78 +334,67 @@ func getProvider(name *C.char) *C.char {
|
||||
|
||||
//export getExternalProviders
|
||||
func getExternalProviders() *C.char {
|
||||
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 := make([]ExternalProvider, 0)
|
||||
for _, p := range externalProviders {
|
||||
externalProvider, err := toExternalProvider(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
eps = append(eps, *externalProvider)
|
||||
}
|
||||
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)
|
||||
sort.Sort(ExternalProviders(eps))
|
||||
data, err := json.Marshal(eps)
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export updateExternalProvider
|
||||
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) {
|
||||
//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) {
|
||||
i := int64(port)
|
||||
providerNameString := C.GoString(providerName)
|
||||
providerTypeString := C.GoString(providerType)
|
||||
geoTypeString := C.GoString(geoType)
|
||||
geoNameString := C.GoString(geoName)
|
||||
go func() {
|
||||
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
|
||||
}
|
||||
switch geoTypeString {
|
||||
case "MMDB":
|
||||
err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString))
|
||||
err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "ASN":
|
||||
err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString))
|
||||
err := updater.UpdateASN(constant.Path.Resolve(geoNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoIp":
|
||||
err := geodata.DownloadGeoIP(constant.Path.Resolve(providerNameString))
|
||||
err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoSite":
|
||||
err := geodata.DownloadGeoSite(constant.Path.Resolve(providerNameString))
|
||||
err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
@@ -426,6 +404,45 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l
|
||||
}()
|
||||
}
|
||||
|
||||
//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)
|
||||
@@ -463,7 +480,7 @@ func init() {
|
||||
Data: c,
|
||||
})
|
||||
}
|
||||
executor.DefaultProxyProviderLoadedHook = func(providerName string) {
|
||||
executor.DefaultProviderLoadedHook = func(providerName string) {
|
||||
SendMessage(Message{
|
||||
Type: LoadedMessage,
|
||||
Data: providerName,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build android
|
||||
|
||||
package main
|
||||
|
||||
import "C"
|
||||
@@ -16,16 +14,24 @@ type AccessControl struct {
|
||||
}
|
||||
|
||||
type AndroidProps struct {
|
||||
Enable bool `json:"enable"`
|
||||
AccessControl *AccessControl `json:"accessControl"`
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
}
|
||||
|
||||
var androidProps AndroidProps
|
||||
type State struct {
|
||||
AndroidProps
|
||||
CurrentProfileName string `json:"currentProfileName"`
|
||||
MixedPort int `json:"mixedPort"`
|
||||
OnlyProxy bool `json:"onlyProxy"`
|
||||
}
|
||||
|
||||
//export getAndroidProps
|
||||
func getAndroidProps() *C.char {
|
||||
data, err := json.Marshal(androidProps)
|
||||
var state State
|
||||
|
||||
//export getState
|
||||
func getState() *C.char {
|
||||
data, err := json.Marshal(state)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
@@ -33,10 +39,10 @@ func getAndroidProps() *C.char {
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export setAndroidProps
|
||||
func setAndroidProps(s *C.char) {
|
||||
//export setState
|
||||
func setState(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
err := json.Unmarshal([]byte(paramsString), &androidProps)
|
||||
err := json.Unmarshal([]byte(paramsString), &state)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
26
core/tun.go
26
core/tun.go
@@ -7,14 +7,15 @@ 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
|
||||
@@ -40,6 +41,18 @@ 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()
|
||||
@@ -88,6 +101,7 @@ func getRunTime() *C.char {
|
||||
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
removeSocketHook()
|
||||
go func() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
@@ -125,7 +139,7 @@ func markSocket(fd Fd) {
|
||||
|
||||
var fdCounter int64 = 0
|
||||
|
||||
func init() {
|
||||
func initSocketHook() {
|
||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||
if platform.ShouldBlockConnection() {
|
||||
return errBlocked
|
||||
@@ -159,3 +173,7 @@ func init() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func removeSocketHook() {
|
||||
dialer.DefaultSocketHook = nil
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ 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)
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -88,7 +89,6 @@ class ApplicationState extends State<Application> {
|
||||
}
|
||||
await globalState.appController.init();
|
||||
globalState.appController.initLink();
|
||||
_updateGroups();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,7 +96,9 @@ class ApplicationState extends State<Application> {
|
||||
if (system.isDesktop) {
|
||||
return WindowContainer(
|
||||
child: TrayContainer(
|
||||
child: app,
|
||||
child: ProxyContainer(
|
||||
child: app,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -120,76 +122,67 @@ class ApplicationState extends State<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_updateGroups() {
|
||||
if (globalState.groupsUpdateTimer != null) {
|
||||
globalState.groupsUpdateTimer?.cancel();
|
||||
globalState.groupsUpdateTimer = null;
|
||||
}
|
||||
globalState.groupsUpdateTimer ??= Timer.periodic(
|
||||
httpTimeoutDuration,
|
||||
(timer) async {
|
||||
await globalState.appController.updateGroups();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return AppStateContainer(
|
||||
child: ClashMessageContainer(
|
||||
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.locale,
|
||||
themeMode: config.themeMode,
|
||||
primaryColor: config.primaryColor,
|
||||
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(),
|
||||
),
|
||||
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 PopContainer(
|
||||
child: _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,
|
||||
),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -100,22 +100,6 @@ 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();
|
||||
@@ -128,8 +112,7 @@ class ClashCore {
|
||||
UsedProxy.GLOBAL.name,
|
||||
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
|
||||
final proxy = proxies[e] ?? {};
|
||||
return GroupTypeExtension.valueList.contains(proxy['type']) &&
|
||||
proxy['hidden'] != true;
|
||||
return GroupTypeExtension.valueList.contains(proxy['type']);
|
||||
})
|
||||
];
|
||||
final groupsRaw = groupNames.map((groupName) {
|
||||
@@ -142,7 +125,11 @@ class ClashCore {
|
||||
.toList();
|
||||
return group;
|
||||
}).toList();
|
||||
return groupsRaw.map((e) => Group.fromJson(e)).toList();
|
||||
return groupsRaw
|
||||
.map(
|
||||
(e) => Group.fromJson(e),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,9 +149,46 @@ class ClashCore {
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> updateExternalProvider({
|
||||
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 providerType,
|
||||
required String data,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
@@ -175,14 +199,34 @@ class ClashCore {
|
||||
}
|
||||
});
|
||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||
final providerTypeChar = providerType.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateExternalProvider(
|
||||
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,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateExternalProvider(
|
||||
providerNameChar,
|
||||
providerTypeChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(providerNameChar);
|
||||
malloc.free(providerTypeChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@@ -193,6 +237,14 @@ class ClashCore {
|
||||
malloc.free(paramsChar);
|
||||
}
|
||||
|
||||
start() {
|
||||
clashFFI.start();
|
||||
}
|
||||
|
||||
stop() {
|
||||
clashFFI.stop();
|
||||
}
|
||||
|
||||
Future<Delay> getDelay(String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
@@ -213,21 +265,13 @@ 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 path) {
|
||||
final pathChar = path.toNativeUtf8().cast<Char>();
|
||||
clashFFI.clearEffect(pathChar);
|
||||
malloc.free(pathChar);
|
||||
clearEffect(String profileId) {
|
||||
final profileIdChar = profileId.toNativeUtf8().cast<Char>();
|
||||
clashFFI.clearEffect(profileIdChar);
|
||||
malloc.free(profileIdChar);
|
||||
}
|
||||
|
||||
VersionInfo getVersionInfo() {
|
||||
@@ -237,19 +281,19 @@ class ClashCore {
|
||||
return VersionInfo.fromJson(versionInfo);
|
||||
}
|
||||
|
||||
setProps(Props props) {
|
||||
final propsChar = json.encode(props).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setAndroidProps(propsChar);
|
||||
malloc.free(propsChar);
|
||||
setState(CoreState state) {
|
||||
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setState(stateChar);
|
||||
malloc.free(stateChar);
|
||||
}
|
||||
|
||||
Props getProps() {
|
||||
final androidPropsRaw = clashFFI.getAndroidProps();
|
||||
final androidProps = json.decode(
|
||||
androidPropsRaw.cast<Utf8>().toDartString(),
|
||||
CoreState getState() {
|
||||
final stateRaw = clashFFI.getState();
|
||||
final state = json.decode(
|
||||
stateRaw.cast<Utf8>().toDartString(),
|
||||
);
|
||||
clashFFI.freeCString(androidPropsRaw);
|
||||
return Props.fromJson(androidProps);
|
||||
clashFFI.freeCString(stateRaw);
|
||||
return CoreState.fromJson(state);
|
||||
}
|
||||
|
||||
Traffic getTraffic() {
|
||||
@@ -319,11 +363,15 @@ class ClashCore {
|
||||
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
closeConnections(String id) {
|
||||
closeConnection(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.closeConnection(idChar);
|
||||
malloc.free(idChar);
|
||||
}
|
||||
|
||||
closeConnections() {
|
||||
clashFFI.closeConnections();
|
||||
}
|
||||
}
|
||||
|
||||
final clashCore = ClashCore();
|
||||
|
||||
@@ -5144,6 +5144,22 @@ 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,
|
||||
) {
|
||||
@@ -5190,30 +5206,6 @@ 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,
|
||||
@@ -5351,16 +5343,16 @@ class ClashFFI {
|
||||
late final _getConnections =
|
||||
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
int closeConnections() {
|
||||
void closeConnections() {
|
||||
return _closeConnections();
|
||||
}
|
||||
|
||||
late final _closeConnectionsPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('closeConnections');
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('closeConnections');
|
||||
late final _closeConnections =
|
||||
_closeConnectionsPtr.asFunction<int Function()>();
|
||||
_closeConnectionsPtr.asFunction<void Function()>();
|
||||
|
||||
int closeConnection(
|
||||
void closeConnection(
|
||||
ffi.Pointer<ffi.Char> id,
|
||||
) {
|
||||
return _closeConnection(
|
||||
@@ -5369,10 +5361,10 @@ class ClashFFI {
|
||||
}
|
||||
|
||||
late final _closeConnectionPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'closeConnection');
|
||||
late final _closeConnection =
|
||||
_closeConnectionPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
|
||||
_closeConnectionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getProviders() {
|
||||
return _getProviders();
|
||||
@@ -5409,24 +5401,76 @@ 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)>>('updateExternalProvider');
|
||||
late final _updateExternalProvider = _updateExternalProviderPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
||||
ffi.LongLong)>>('sideLoadExternalProvider');
|
||||
late final _sideLoadExternalProvider =
|
||||
_sideLoadExternalProviderPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void initNativeApiBridge(
|
||||
ffi.Pointer<ffi.Void> api,
|
||||
@@ -5499,29 +5543,28 @@ class ClashFFI {
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getAndroidProps() {
|
||||
return _getAndroidProps();
|
||||
ffi.Pointer<ffi.Char> getState() {
|
||||
return _getState();
|
||||
}
|
||||
|
||||
late final _getAndroidPropsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getAndroidProps');
|
||||
late final _getAndroidProps =
|
||||
_getAndroidPropsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
late final _getStatePtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('getState');
|
||||
late final _getState =
|
||||
_getStatePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void setAndroidProps(
|
||||
void setState(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _setAndroidProps(
|
||||
return _setState(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setAndroidPropsPtr =
|
||||
late final _setStatePtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setAndroidProps');
|
||||
late final _setAndroidProps =
|
||||
_setAndroidPropsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
'setState');
|
||||
late final _setState =
|
||||
_setStatePtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void startTUN(
|
||||
int fd,
|
||||
|
||||
28
lib/common/archive.dart
Normal file
28
lib/common/archive.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,21 @@ 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 {
|
||||
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
|
||||
? copyWith(
|
||||
surface: Colors.black,
|
||||
background: Colors.black,
|
||||
surfaceContainer: surfaceContainer.darken(0.05),
|
||||
)
|
||||
: this;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ export 'app_localizations.dart';
|
||||
export 'function.dart';
|
||||
export 'package.dart';
|
||||
export 'measure.dart';
|
||||
export 'service.dart';
|
||||
export 'windows.dart';
|
||||
export 'iterable.dart';
|
||||
export 'scroll.dart';
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
@@ -8,7 +7,7 @@ import 'system.dart';
|
||||
|
||||
const appName = "FlClash";
|
||||
const coreName = "clash.meta";
|
||||
const packageName = "FlClash";
|
||||
const packageName = "com.follow.clash";
|
||||
const httpTimeoutDuration = Duration(milliseconds: 5000);
|
||||
const moreDuration = Duration(milliseconds: 100);
|
||||
const animateDuration = Duration(milliseconds: 100);
|
||||
@@ -17,7 +16,7 @@ const mmdbFileName = "geoip.metadb";
|
||||
const asnFileName = "ASN.mmdb";
|
||||
const geoIpFileName = "GeoIP.dat";
|
||||
const geoSiteFileName = "GeoSite.dat";
|
||||
final double kHeaderHeight = system.isDesktop ? (Platform.isMacOS ? 28 : 40) : 0;
|
||||
final double kHeaderHeight = system.isDesktop ? 40 : 0;
|
||||
const GeoXMap defaultGeoXMap = {
|
||||
"mmdb":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
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 {
|
||||
@@ -33,8 +30,6 @@ class DAVClient {
|
||||
Future<bool> _ping() async {
|
||||
try {
|
||||
await client.ping();
|
||||
await client.mkdir("/$appName");
|
||||
await client.mkdir("/$appName/$profilesDirectoryName");
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
@@ -43,65 +38,17 @@ class DAVClient {
|
||||
|
||||
get root => "/$appName";
|
||||
|
||||
get remoteConfig => "$root/$configKey.json";
|
||||
get backupFile => "$root/backup.zip";
|
||||
|
||||
get remoteClashConfig => "$root/$clashConfigKey.json";
|
||||
|
||||
get remoteProfiles => "$root/$profilesDirectoryName";
|
||||
|
||||
backup() async {
|
||||
final appController = globalState.appController;
|
||||
final config = appController.config;
|
||||
final clashConfig = appController.clashConfig;
|
||||
backup(Uint8List data) async {
|
||||
await client.mkdir("$root");
|
||||
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)}",
|
||||
);
|
||||
}
|
||||
await client.write("$backupFile", data);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
Future<List<int>> recovery() async {
|
||||
await client.mkdir("$root");
|
||||
final data = await client.read(backupFile);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,58 @@ extension IterableExt<T> on Iterable<T> {
|
||||
yield iterator.current;
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<List<T>> chunks(int size) sync* {
|
||||
if (length == 0) return;
|
||||
var iterator = this.iterator;
|
||||
while (iterator.moveNext()) {
|
||||
var chunk = [iterator.current];
|
||||
for (var i = 1; i < size && iterator.moveNext(); i++) {
|
||||
chunk.add(iterator.current);
|
||||
}
|
||||
yield chunk;
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<T> fill(
|
||||
int length, {
|
||||
required T Function(int count) filler,
|
||||
}) sync* {
|
||||
int count = 0;
|
||||
for (var item in this) {
|
||||
yield item;
|
||||
count++;
|
||||
if (count >= length) return;
|
||||
}
|
||||
while (count < length) {
|
||||
yield filler(count);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DoubleListExt on List<double> {
|
||||
int findInterval(num target) {
|
||||
if (isEmpty) return -1;
|
||||
if (target < first) return -1;
|
||||
if (target >= last) return length - 1;
|
||||
|
||||
int left = 0;
|
||||
int right = length - 1;
|
||||
|
||||
while (left <= right) {
|
||||
int mid = left + (right - left) ~/ 2;
|
||||
|
||||
if (mid == length - 1 ||
|
||||
(this[mid] <= target && target < this[mid + 1])) {
|
||||
return mid;
|
||||
} else if (target < this[mid]) {
|
||||
right = mid - 1;
|
||||
} else {
|
||||
left = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1; // 这行理论上不会执行到,但为了完整性保留
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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;
|
||||
@@ -24,18 +26,77 @@ 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(bool value) async {
|
||||
final isEnable = await this.isEnable;
|
||||
if (isEnable == value) return;
|
||||
if (value == true) {
|
||||
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) {
|
||||
enable();
|
||||
} else {
|
||||
disable();
|
||||
|
||||
@@ -44,7 +44,7 @@ class Navigation {
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.swap_vert_circle),
|
||||
icon: Icon(Icons.storage),
|
||||
label: "resources",
|
||||
description: "resourcesDesc",
|
||||
keep: false,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/common/app_localizations.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/common/common.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.compareTo(charB);
|
||||
return charA.compareToLower(charB);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,22 +191,21 @@ class Other {
|
||||
return ViewMode.desktop;
|
||||
}
|
||||
|
||||
int getColumns(ViewMode viewMode, int currentColumns) {
|
||||
final targetColumnsArray = viewModeColumnsMap[viewMode]!;
|
||||
if (targetColumnsArray.contains(currentColumns)) {
|
||||
return currentColumns;
|
||||
}
|
||||
return targetColumnsArray.first;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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');
|
||||
@@ -23,6 +24,9 @@ class AppPath {
|
||||
getApplicationSupportDirectory().then((value) {
|
||||
cacheDir.complete(value);
|
||||
});
|
||||
getDownloadsDirectory().then((value) {
|
||||
downloadDir.complete(value);
|
||||
});
|
||||
// if (Platform.isAndroid) {
|
||||
// getApplicationSupportDirectory().then((value) {
|
||||
// cacheDir.complete(value);
|
||||
@@ -39,6 +43,11 @@ 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;
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
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?> pickerConfigFile() async {
|
||||
Future<PlatformFile?> pickerFile() async {
|
||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
allowMultiple: false,
|
||||
initialDirectory: await appPath.getDownloadDirPath(),
|
||||
);
|
||||
return filePickerResult?.files.first;
|
||||
}
|
||||
|
||||
Future<PlatformFile?> pickerGeoDataFile() async {
|
||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
allowMultiple: false,
|
||||
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,
|
||||
);
|
||||
return filePickerResult?.files.first;
|
||||
if (!Platform.isAndroid && path != null) {
|
||||
final file = await File(path).create(recursive: true);
|
||||
await file.writeAsBytes(bytes);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Future<String?> pickerConfigQRCode() async {
|
||||
|
||||
@@ -1,37 +1,4 @@
|
||||
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';
|
||||
import 'package:fl_clash/common/system.dart';
|
||||
import 'package:proxy/proxy.dart';
|
||||
|
||||
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();
|
||||
final proxy = system.isDesktop ? Proxy() : null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
@@ -13,9 +14,6 @@ class Request {
|
||||
|
||||
Request() {
|
||||
_dio = Dio();
|
||||
_dio.options = BaseOptions(
|
||||
headers: {"User-Agent": globalState.appController.clashConfig.globalUa},
|
||||
);
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
@@ -52,11 +50,14 @@ class Request {
|
||||
.get(
|
||||
url,
|
||||
options: Options(
|
||||
headers: {
|
||||
"User-Agent": globalState.appController.clashConfig.globalUa
|
||||
},
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
)
|
||||
.timeout(
|
||||
httpTimeoutDuration * 2,
|
||||
httpTimeoutDuration * 6,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
@@ -85,11 +86,14 @@ class Request {
|
||||
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||
};
|
||||
|
||||
Future<IpInfo?> checkIp(CancelToken? cancelToken) async {
|
||||
for (final source in _ipInfoSources.entries) {
|
||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||
for (final source in _ipInfoSources.entries.toList()..shuffle(Random())) {
|
||||
try {
|
||||
final response = await _dio
|
||||
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
|
||||
.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
)
|
||||
.timeout(
|
||||
httpTimeoutDuration,
|
||||
);
|
||||
@@ -97,6 +101,9 @@ class Request {
|
||||
return source.value(response.data!);
|
||||
}
|
||||
} catch (e) {
|
||||
if(cancelToken?.isCancelled == true){
|
||||
throw "cancelled";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,3 +14,14 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
|
||||
PointerDeviceKind.unknown,
|
||||
};
|
||||
}
|
||||
|
||||
class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
||||
@override
|
||||
Widget buildScrollbar(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
typedef CreateServiceNative = IntPtr Function(
|
||||
IntPtr hSCManager,
|
||||
Pointer<Utf16> lpServiceName,
|
||||
Pointer<Utf16> lpDisplayName,
|
||||
Uint32 dwDesiredAccess,
|
||||
Uint32 dwServiceType,
|
||||
Uint32 dwStartType,
|
||||
Uint32 dwErrorControl,
|
||||
Pointer<Utf16> lpBinaryPathName,
|
||||
Pointer<Utf16> lpLoadOrderGroup,
|
||||
Pointer<Uint32> lpdwTagId,
|
||||
Pointer<Utf16> lpDependencies,
|
||||
Pointer<Utf16> lpServiceStartName,
|
||||
Pointer<Utf16> lpPassword,
|
||||
);
|
||||
|
||||
typedef CreateServiceDart = int Function(
|
||||
int hSCManager,
|
||||
Pointer<Utf16> lpServiceName,
|
||||
Pointer<Utf16> lpDisplayName,
|
||||
int dwDesiredAccess,
|
||||
int dwServiceType,
|
||||
int dwStartType,
|
||||
int dwErrorControl,
|
||||
Pointer<Utf16> lpBinaryPathName,
|
||||
Pointer<Utf16> lpLoadOrderGroup,
|
||||
Pointer<Uint32> lpdwTagId,
|
||||
Pointer<Utf16> lpDependencies,
|
||||
Pointer<Utf16> lpServiceStartName,
|
||||
Pointer<Utf16> lpPassword,
|
||||
);
|
||||
|
||||
const _SERVICE_ALL_ACCESS = 0xF003F;
|
||||
|
||||
const _SERVICE_WIN32_OWN_PROCESS = 0x00000010;
|
||||
|
||||
const _SERVICE_AUTO_START = 0x00000002;
|
||||
|
||||
const _SERVICE_ERROR_NORMAL = 0x00000001;
|
||||
|
||||
typedef GetLastErrorNative = Uint32 Function();
|
||||
typedef GetLastErrorDart = int Function();
|
||||
|
||||
class Service {
|
||||
static Service? _instance;
|
||||
late DynamicLibrary _advapi32;
|
||||
|
||||
Service._internal() {
|
||||
_advapi32 = DynamicLibrary.open('advapi32.dll');
|
||||
}
|
||||
|
||||
factory Service() {
|
||||
_instance ??= Service._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> createService() async {
|
||||
final int scManager = OpenSCManager(nullptr, nullptr, _SERVICE_ALL_ACCESS);
|
||||
if (scManager == 0) return;
|
||||
final serviceName = 'FlClash Service'.toNativeUtf16();
|
||||
final displayName = 'FlClash Service'.toNativeUtf16();
|
||||
final binaryPathName = "C:\\Application\\Clash.Verge_1.6.6_x64_portable\\resources\\clash-verge-service.exe".toNativeUtf16();
|
||||
final createService =
|
||||
_advapi32.lookupFunction<CreateServiceNative, CreateServiceDart>(
|
||||
'CreateServiceW',
|
||||
);
|
||||
final getLastError = DynamicLibrary.open('kernel32.dll')
|
||||
.lookupFunction<GetLastErrorNative, GetLastErrorDart>('GetLastError');
|
||||
|
||||
final serviceHandle = createService(
|
||||
scManager,
|
||||
serviceName,
|
||||
displayName,
|
||||
_SERVICE_ALL_ACCESS,
|
||||
_SERVICE_WIN32_OWN_PROCESS,
|
||||
_SERVICE_AUTO_START,
|
||||
_SERVICE_ERROR_NORMAL,
|
||||
binaryPathName,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
);
|
||||
|
||||
print("serviceHandle $serviceHandle");
|
||||
|
||||
final errorCode = GetLastError();
|
||||
print('Error code: $errorCode');
|
||||
|
||||
final result = StartService(serviceHandle, 0, nullptr);
|
||||
|
||||
if (result == 0) {
|
||||
print('Failed to start the service.');
|
||||
} else {
|
||||
print('Service started successfully.');
|
||||
}
|
||||
|
||||
calloc.free(serviceName);
|
||||
calloc.free(displayName);
|
||||
calloc.free(binaryPathName);
|
||||
}
|
||||
}
|
||||
|
||||
final service = Platform.isWindows ? Service() : null;
|
||||
@@ -2,4 +2,10 @@ extension StringExtension on String {
|
||||
bool get isUrl {
|
||||
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
|
||||
}
|
||||
|
||||
int compareToLower(String other) {
|
||||
return toLowerCase().compareTo(
|
||||
other.toLowerCase(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ 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();
|
||||
|
||||
@@ -19,7 +19,8 @@ class Window {
|
||||
await windowManager.ensureInitialized();
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(props.width, props.height),
|
||||
minimumSize: const Size(380, 600),
|
||||
minimumSize: const Size(380, 500),
|
||||
windowButtonVisibility: false,
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
);
|
||||
if (props.left != null || props.top != null) {
|
||||
@@ -33,7 +34,7 @@ class Window {
|
||||
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
// }
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.setPreventClose(true);
|
||||
// await windowManager.setPreventClose(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
59
lib/common/windows.dart
Normal file
59
lib/common/windows.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
class Windows {
|
||||
static Windows? _instance;
|
||||
late DynamicLibrary _shell32;
|
||||
|
||||
Windows._internal() {
|
||||
_shell32 = DynamicLibrary.open('shell32.dll');
|
||||
}
|
||||
|
||||
factory Windows() {
|
||||
_instance ??= Windows._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
bool runas(String command, String arguments) {
|
||||
final commandPtr = command.toNativeUtf16();
|
||||
final argumentsPtr = arguments.toNativeUtf16();
|
||||
final operationPtr = 'runas'.toNativeUtf16();
|
||||
|
||||
final shellExecute = _shell32.lookupFunction<
|
||||
Int32 Function(
|
||||
Pointer<Utf16> hwnd,
|
||||
Pointer<Utf16> lpOperation,
|
||||
Pointer<Utf16> lpFile,
|
||||
Pointer<Utf16> lpParameters,
|
||||
Pointer<Utf16> lpDirectory,
|
||||
Int32 nShowCmd),
|
||||
int Function(
|
||||
Pointer<Utf16> hwnd,
|
||||
Pointer<Utf16> lpOperation,
|
||||
Pointer<Utf16> lpFile,
|
||||
Pointer<Utf16> lpParameters,
|
||||
Pointer<Utf16> lpDirectory,
|
||||
int nShowCmd)>('ShellExecuteW');
|
||||
|
||||
final result = shellExecute(
|
||||
nullptr,
|
||||
operationPtr,
|
||||
commandPtr,
|
||||
argumentsPtr,
|
||||
nullptr,
|
||||
1,
|
||||
);
|
||||
|
||||
calloc.free(commandPtr);
|
||||
calloc.free(argumentsPtr);
|
||||
calloc.free(operationPtr);
|
||||
|
||||
if (result <= 32) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final windows = Platform.isWindows ? Windows() : null;
|
||||
@@ -1,8 +1,15 @@
|
||||
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';
|
||||
|
||||
@@ -17,7 +24,9 @@ class AppController {
|
||||
late ClashConfig clashConfig;
|
||||
late Measure measure;
|
||||
late Function updateClashConfigDebounce;
|
||||
late Function updateGroupDebounce;
|
||||
late Function addCheckIpNumDebounce;
|
||||
late Function applyProfileDebounce;
|
||||
|
||||
AppController(this.context) {
|
||||
appState = context.read<AppState>();
|
||||
@@ -26,16 +35,21 @@ class AppController {
|
||||
updateClashConfigDebounce = debounce<Function()>(() async {
|
||||
await updateClashConfig();
|
||||
});
|
||||
applyProfileDebounce = debounce<Function()>(() async {
|
||||
await applyProfile(isPrue: true);
|
||||
});
|
||||
addCheckIpNumDebounce = debounce(() {
|
||||
appState.checkIpNum++;
|
||||
});
|
||||
updateGroupDebounce = debounce(() async {
|
||||
await updateGroups();
|
||||
});
|
||||
measure = Measure.of(context);
|
||||
}
|
||||
|
||||
Future<void> updateSystemProxy(bool isStart) async {
|
||||
updateStatus(bool isStart) async {
|
||||
if (isStart) {
|
||||
await globalState.startSystemProxy(
|
||||
appState: appState,
|
||||
await globalState.handleStart(
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
@@ -45,12 +59,14 @@ class AppController {
|
||||
updateRunTime,
|
||||
updateTraffic,
|
||||
];
|
||||
applyProfileDebounce();
|
||||
} else {
|
||||
await globalState.stopSystemProxy();
|
||||
await globalState.handleStop();
|
||||
clashCore.resetTraffic();
|
||||
appState.traffics = [];
|
||||
appState.totalTraffic = Traffic();
|
||||
appState.runTime = null;
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +75,9 @@ class AppController {
|
||||
}
|
||||
|
||||
updateRunTime() {
|
||||
if (proxyManager.startTime != null) {
|
||||
final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch;
|
||||
final startTime = globalState.startTime;
|
||||
if (startTime != null) {
|
||||
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
||||
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||
appState.runTime = nowTimeStamp - startTimeStamp;
|
||||
} else {
|
||||
@@ -82,22 +99,22 @@ class AppController {
|
||||
|
||||
deleteProfile(String id) async {
|
||||
config.deleteProfileById(id);
|
||||
final profilePath = await appPath.getProfilePath(id);
|
||||
if (profilePath == null) return;
|
||||
clashCore.clearEffect(profilePath);
|
||||
clashCore.clearEffect(id);
|
||||
if (config.currentProfileId == id) {
|
||||
if (config.profiles.isNotEmpty) {
|
||||
final updateId = config.profiles.first.id;
|
||||
changeProfile(updateId);
|
||||
} else {
|
||||
changeProfile(null);
|
||||
updateStatus(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateProfile(Profile profile) async {
|
||||
await profile.update();
|
||||
config.setProfile(await profile.update());
|
||||
final newProfile = await profile.update();
|
||||
config.setProfile(
|
||||
newProfile.copyWith(isUpdating: false),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||
@@ -108,32 +125,30 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
Future applyProfile() async {
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
commonScaffoldState?.loadingRun(() async {
|
||||
Future applyProfile({bool isPrue = false}) async {
|
||||
if (isPrue) {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future rawApplyProfile() async {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
} else {
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
});
|
||||
}
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
changeProfile(String? value) async {
|
||||
if (value == config.currentProfileId) return;
|
||||
config.currentProfileId = value;
|
||||
await applyProfile();
|
||||
appState.delayMap = {};
|
||||
saveConfigPreferences();
|
||||
}
|
||||
|
||||
autoUpdateProfiles() async {
|
||||
@@ -192,6 +207,18 @@ class AppController {
|
||||
await preferences.saveClashConfig(clashConfig);
|
||||
}
|
||||
|
||||
changeProxy({
|
||||
required String groupName,
|
||||
required String proxyName,
|
||||
}) {
|
||||
globalState.changeProxy(
|
||||
config: config,
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
);
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
handleBackOrExit() async {
|
||||
if (config.isMinimizeOnExit) {
|
||||
if (system.isDesktop) {
|
||||
@@ -204,7 +231,7 @@ class AppController {
|
||||
}
|
||||
|
||||
handleExit() async {
|
||||
await updateSystemProxy(false);
|
||||
await updateStatus(false);
|
||||
await savePreferences();
|
||||
clashCore.shutdown();
|
||||
system.exit();
|
||||
@@ -273,31 +300,13 @@ class AppController {
|
||||
if (!config.silentLaunch) {
|
||||
window?.show();
|
||||
}
|
||||
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 globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
globalState.updateStartTime();
|
||||
}
|
||||
await afterInit();
|
||||
}
|
||||
|
||||
afterInit() async {
|
||||
await proxyManager.updateStartTime();
|
||||
if (proxyManager.isStart) {
|
||||
await updateSystemProxy(true);
|
||||
if (globalState.isStart) {
|
||||
await updateStatus(true);
|
||||
} else {
|
||||
await updateSystemProxy(config.autoRun);
|
||||
await updateStatus(config.autoRun);
|
||||
}
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
@@ -361,6 +370,10 @@ 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);
|
||||
@@ -382,7 +395,11 @@ class AppController {
|
||||
}
|
||||
|
||||
addProfileFormFile() async {
|
||||
final platformFile = await globalState.safeRun(picker.pickerConfigFile);
|
||||
final platformFile = await globalState.safeRun(picker.pickerFile);
|
||||
final bytes = platformFile?.bytes;
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
||||
toProfiles();
|
||||
@@ -391,10 +408,6 @@ class AppController {
|
||||
final profile = await commonScaffoldState?.loadingRun<Profile?>(
|
||||
() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
final bytes = platformFile?.bytes;
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return await Profile.normal(label: platformFile?.name).saveFile(bytes);
|
||||
},
|
||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||
@@ -410,9 +423,6 @@ class AppController {
|
||||
addProfileFormURL(url);
|
||||
}
|
||||
|
||||
int get columns =>
|
||||
other.getColumns(appState.viewMode, config.proxiesColumns);
|
||||
|
||||
updateViewWidth(double width) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appState.viewWidth = width;
|
||||
@@ -422,7 +432,10 @@ class AppController {
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) => other.sortByChar(a.name, b.name),
|
||||
(a, b) => other.sortByChar(
|
||||
PinyinHelper.getPinyin(a.name),
|
||||
PinyinHelper.getPinyin(b.name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -453,4 +466,70 @@ class AppController {
|
||||
ProxiesSortType.name => _sortOfName(proxies),
|
||||
};
|
||||
}
|
||||
|
||||
String getCurrentSelectedName(String groupName) {
|
||||
final group = appState.getGroupWithName(groupName);
|
||||
return group?.getCurrentSelectedName(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ enum TunStack { gvisor, system, mixed }
|
||||
|
||||
enum AccessControlMode { acceptSelected, rejectSelected }
|
||||
|
||||
enum AccessSortType { none, name, time }
|
||||
|
||||
enum ProfileType { file, url }
|
||||
|
||||
enum ResultType { success, error }
|
||||
@@ -84,4 +86,6 @@ enum CommonCardType { plain, filled }
|
||||
|
||||
enum ProxiesType { tab, list }
|
||||
|
||||
enum ProxiesLayout{ loose, standard, tight }
|
||||
|
||||
enum ProxyCardType { expand, shrink, min }
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@immutable
|
||||
class Contributor {
|
||||
@@ -90,7 +89,7 @@ class AboutFragment extends StatelessWidget {
|
||||
];
|
||||
return generateSection(
|
||||
separated: false,
|
||||
title: appLocalizations.contributors,
|
||||
title: appLocalizations.otherContributors,
|
||||
items: [
|
||||
ListItem(
|
||||
title: SingleChildScrollView(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
@@ -6,15 +7,9 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/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});
|
||||
|
||||
@@ -23,198 +18,164 @@ class AccessFragment extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AccessFragmentState extends State<AccessFragment> {
|
||||
final packagesListenable = ValueNotifier<List<Package>>([]);
|
||||
List<String> acceptList = [];
|
||||
List<String> rejectList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateInitList();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||
packagesListenable.value = await app?.getPackages() ?? [];
|
||||
});
|
||||
final appState = globalState.appController.appState;
|
||||
if (appState.packages.isEmpty) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||
appState.packages = await app?.getPackages() ?? [];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
packagesListenable.dispose();
|
||||
_updateInitList() {
|
||||
final accessControl = globalState.appController.config.accessControl;
|
||||
acceptList = accessControl.acceptList;
|
||||
rejectList = accessControl.rejectList;
|
||||
}
|
||||
|
||||
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) {
|
||||
Widget _buildSearchButton() {
|
||||
return IconButton(
|
||||
tooltip: appLocalizations.search,
|
||||
onPressed: () {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: AccessControlSearchDelegate(
|
||||
packages: packages,
|
||||
acceptList: acceptList,
|
||||
rejectList: rejectList,
|
||||
),
|
||||
).then((_) => {setState(() {})});
|
||||
).then((_) => setState(() {
|
||||
_updateInitList();
|
||||
}));
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
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 _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: FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: _buildSelectedAllButton(
|
||||
isAccessControl: isAccessControl,
|
||||
isSelectedAll: valueList.length == packageNameList.length,
|
||||
allValueList: packageNameList,
|
||||
),
|
||||
Widget _buildSettingButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
title: appLocalizations.proxiesSetting,
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AccessControlWidget(
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.tune),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.isAccessControl,
|
||||
builder: (_, isAccessControl, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: ListItem.switchItem(
|
||||
title: Text(appLocalizations.appAccessControl),
|
||||
delegate: SwitchDelegate(
|
||||
value: isAccessControl,
|
||||
onChanged: (isAccessControl) {
|
||||
final config = context.read<Config>();
|
||||
config.isAccessControl = isAccessControl;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Divider(
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: child!,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Selector<AppState, List<Package>>(
|
||||
selector: (_, appState) => appState.packages,
|
||||
builder: (_, packages, ___) {
|
||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
||||
selector: (_, appState, config) => PackageListSelectorState(
|
||||
accessControl: config.accessControl,
|
||||
isAccessControl: config.isAccessControl,
|
||||
packages: appState.packages,
|
||||
),
|
||||
builder: (context, state, __) {
|
||||
final accessControl = state.accessControl;
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages = state.getList(
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? acceptList
|
||||
: rejectList,
|
||||
);
|
||||
final currentList = accessControl.currentList;
|
||||
final packageNameList =
|
||||
packages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
final describe =
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? appLocalizations.accessControlAllowDesc
|
||||
: appLocalizations.accessControlNotAllowDesc;
|
||||
return DisabledMask(
|
||||
status: !isAccessControl,
|
||||
child: Column(
|
||||
children: [
|
||||
AbsorbPointer(
|
||||
@@ -285,9 +246,18 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: _buildSearchButton(currentPackages)),
|
||||
Flexible(child: _buildFilterSystemAppButton()),
|
||||
Flexible(child: _buildAppProxyModePopup()),
|
||||
child: _buildSearchButton(),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSelectedAllButton(
|
||||
isSelectedAll: valueList.length ==
|
||||
packageNameList.length,
|
||||
allValueList: packageNameList,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSettingButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -296,92 +266,52 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
),
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.isAccessControl,
|
||||
builder: (_, isAccessControl, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: ListItem.switchItem(
|
||||
title: Text(appLocalizations.appAccessControl),
|
||||
delegate: SwitchDelegate(
|
||||
value: isAccessControl,
|
||||
onChanged: (isAccessControl) {
|
||||
final config = context.read<Config>();
|
||||
config.isAccessControl = isAccessControl;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Divider(
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: child!,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: _buildPackageList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -448,23 +378,14 @@ class PackageListItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
class AccessControlSearchDelegate extends SearchDelegate {
|
||||
final List<Package> packages;
|
||||
List<String> acceptList = [];
|
||||
List<String> rejectList = [];
|
||||
|
||||
AccessControlSearchDelegate({
|
||||
required this.packages,
|
||||
required this.acceptList,
|
||||
required this.rejectList,
|
||||
});
|
||||
|
||||
List<Package> get _results {
|
||||
final lowQuery = query.toLowerCase();
|
||||
return packages
|
||||
.where(
|
||||
(package) =>
|
||||
package.label.toLowerCase().contains(lowQuery) ||
|
||||
package.packageName.contains(lowQuery),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) {
|
||||
return [
|
||||
@@ -494,26 +415,39 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _packageList(List<Package> packages) {
|
||||
return Selector<Config, PackageListSelectorState>(
|
||||
selector: (_, config) => PackageListSelectorState(
|
||||
Widget _packageList() {
|
||||
final lowQuery = query.toLowerCase();
|
||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
||||
selector: (_, appState, config) => PackageListSelectorState(
|
||||
packages: appState.packages,
|
||||
accessControl: config.accessControl,
|
||||
isAccessControl: config.isAccessControl,
|
||||
),
|
||||
builder: (context, state, __) {
|
||||
final accessControl = state.accessControl;
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages = state.getList(
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? acceptList
|
||||
: rejectList,
|
||||
);
|
||||
final queryPackages = packages
|
||||
.where(
|
||||
(package) =>
|
||||
package.label.toLowerCase().contains(lowQuery) ||
|
||||
package.packageName.contains(lowQuery),
|
||||
)
|
||||
.toList();
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final currentList = accessControl.currentList;
|
||||
final packageNameList =
|
||||
this.packages.map((e) => e.packageName).toList();
|
||||
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
return DisabledMask(
|
||||
status: !isAccessControl,
|
||||
child: ListView.builder(
|
||||
itemCount: packages.length,
|
||||
itemCount: queryPackages.length,
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
final package = queryPackages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
@@ -551,6 +485,268 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
return _packageList(_results);
|
||||
return _packageList();
|
||||
}
|
||||
}
|
||||
|
||||
class AccessControlWidget extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
|
||||
const AccessControlWidget({
|
||||
super.key,
|
||||
required this.context,
|
||||
});
|
||||
|
||||
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
||||
return switch (mode) {
|
||||
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
||||
AccessControlMode.rejectSelected => Icons.block_outlined,
|
||||
};
|
||||
}
|
||||
|
||||
String _getTextWithAccessControlMode(AccessControlMode mode) {
|
||||
return switch (mode) {
|
||||
AccessControlMode.acceptSelected => appLocalizations.whitelistMode,
|
||||
AccessControlMode.rejectSelected => appLocalizations.blacklistMode,
|
||||
};
|
||||
}
|
||||
|
||||
String _getTextWithAccessSortType(AccessSortType type) {
|
||||
return switch (type) {
|
||||
AccessSortType.none => appLocalizations.defaultText,
|
||||
AccessSortType.name => appLocalizations.name,
|
||||
AccessSortType.time => appLocalizations.time,
|
||||
};
|
||||
}
|
||||
|
||||
IconData _getIconWithProxiesSortType(AccessSortType type) {
|
||||
return switch (type) {
|
||||
AccessSortType.none => Icons.sort,
|
||||
AccessSortType.name => Icons.sort_by_alpha,
|
||||
AccessSortType.time => Icons.timeline,
|
||||
};
|
||||
}
|
||||
|
||||
String _getTextWithIsFilterSystemApp(bool isFilterSystemApp) {
|
||||
return switch (isFilterSystemApp) {
|
||||
true => appLocalizations.onlyOtherApps,
|
||||
false => appLocalizations.allApps,
|
||||
};
|
||||
}
|
||||
|
||||
List<Widget> _buildModeSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.mode,
|
||||
items: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, AccessControlMode>(
|
||||
selector: (_, config) => config.accessControl.mode,
|
||||
builder: (_, accessControlMode, __) {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
for (final item in AccessControlMode.values)
|
||||
SettingInfoCard(
|
||||
Info(
|
||||
label: _getTextWithAccessControlMode(item),
|
||||
iconData: _getIconWithAccessControlMode(item),
|
||||
),
|
||||
isSelected: accessControlMode == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
mode: item,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSortSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.sort,
|
||||
items: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, AccessSortType>(
|
||||
selector: (_, config) => config.accessControl.sort,
|
||||
builder: (_, accessSortType, __) {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
for (final item in AccessSortType.values)
|
||||
SettingInfoCard(
|
||||
Info(
|
||||
label: _getTextWithAccessSortType(item),
|
||||
iconData: _getIconWithProxiesSortType(item),
|
||||
),
|
||||
isSelected: accessSortType == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
sort: item,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSourceSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.source,
|
||||
items: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.accessControl.isFilterSystemApp,
|
||||
builder: (_, isFilterSystemApp, __) {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
for (final item in [false, true])
|
||||
SettingTextCard(
|
||||
_getTextWithIsFilterSystemApp(item),
|
||||
isSelected: isFilterSystemApp == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
isFilterSystemApp: item,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_intelligentSelected() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final config = globalState.appController.config;
|
||||
final accessControl = config.accessControl;
|
||||
final packageNames = appState.packages
|
||||
.where(
|
||||
(item) =>
|
||||
accessControl.isFilterSystemApp ? item.isSystem == false : true,
|
||||
)
|
||||
.map((item) => item.packageName);
|
||||
Navigator.of(context).pop();
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
final selectedPackageNames =
|
||||
(await commonScaffoldState?.loadingRun<List<String>>(
|
||||
() async {
|
||||
return await app?.getChinaPackageNames() ?? [];
|
||||
},
|
||||
))
|
||||
?.toSet() ??
|
||||
{};
|
||||
final acceptList = packageNames
|
||||
.where((item) => !selectedPackageNames.contains(item))
|
||||
.toList();
|
||||
final rejectList = packageNames
|
||||
.where((item) => selectedPackageNames.contains(item))
|
||||
.toList();
|
||||
config.accessControl = accessControl.copyWith(
|
||||
acceptList: acceptList,
|
||||
rejectList: rejectList,
|
||||
);
|
||||
}
|
||||
|
||||
_copyToClipboard() async {
|
||||
await globalState.safeRun(() {
|
||||
final data = globalState.appController.config.accessControl.toJson();
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: json.encode(data),
|
||||
),
|
||||
);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
_pasteToClipboard() async {
|
||||
await globalState.safeRun(() async {
|
||||
final config = globalState.appController.config;
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text == null) return;
|
||||
config.accessControl = AccessControl.fromJson(
|
||||
json.decode(text),
|
||||
);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
List<Widget> _buildActionSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.action,
|
||||
items: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
spacing: 16,
|
||||
children: [
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.auto_awesome),
|
||||
label: appLocalizations.intelligentSelected,
|
||||
onPressed: _intelligentSelected,
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.paste),
|
||||
label: appLocalizations.clipboardImport,
|
||||
onPressed: _pasteToClipboard,
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.content_copy),
|
||||
label: appLocalizations.clipboardExport,
|
||||
onPressed: _copyToClipboard,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._buildModeSetting(),
|
||||
..._buildSortSetting(),
|
||||
..._buildSourceSetting(),
|
||||
..._buildActionSetting(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class ApplicationSettingFragment extends StatelessWidget {
|
||||
selector: (_, config) => config.autoRun,
|
||||
builder: (_, autoRun, child) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.start),
|
||||
leading: const Icon(Icons.not_started),
|
||||
title: Text(appLocalizations.autoRun),
|
||||
subtitle: Text(appLocalizations.autoRunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
@@ -10,16 +12,9 @@ import 'package:fl_clash/widgets/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class BackupAndRecovery extends StatefulWidget {
|
||||
class BackupAndRecovery extends StatelessWidget {
|
||||
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(
|
||||
@@ -28,11 +23,15 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
|
||||
);
|
||||
}
|
||||
|
||||
_backup() async {
|
||||
_backupOnWebDAV(BuildContext context, DAVClient client) async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(() async {
|
||||
return await _client?.backup();
|
||||
});
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final backupData = await globalState.appController.backupData();
|
||||
return await client.backup(Uint8List.fromList(backupData));
|
||||
},
|
||||
title: appLocalizations.backup,
|
||||
);
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.backup,
|
||||
@@ -40,11 +39,20 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
|
||||
);
|
||||
}
|
||||
|
||||
_recovery(RecoveryOption recoveryOption) async {
|
||||
_recoveryOnWebDAV(
|
||||
BuildContext context,
|
||||
DAVClient client,
|
||||
RecoveryOption recoveryOption,
|
||||
) async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(() async {
|
||||
return await _client?.recovery(recoveryOption: recoveryOption);
|
||||
});
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final data = await client.recovery();
|
||||
await globalState.appController.recoveryData(data, recoveryOption);
|
||||
return true;
|
||||
},
|
||||
title: appLocalizations.recovery,
|
||||
);
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.recovery,
|
||||
@@ -52,12 +60,66 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
|
||||
);
|
||||
}
|
||||
|
||||
_handleRecovery() async {
|
||||
_handleRecoveryOnWebDAV(BuildContext context, DAVClient client) async {
|
||||
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
|
||||
child: const RecoveryOptionsDialog(),
|
||||
);
|
||||
if (recoveryOption == null) return;
|
||||
_recovery(recoveryOption);
|
||||
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);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -65,12 +127,11 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
|
||||
return Selector<Config, DAV?>(
|
||||
selector: (_, config) => config.dav,
|
||||
builder: (_, dav, __) {
|
||||
if (dav == null) {
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(
|
||||
title: appLocalizations.account,
|
||||
),
|
||||
final client = dav != null ? DAVClient(dav) : null;
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.remote),
|
||||
if (dav == null)
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: Text(appLocalizations.noInfo),
|
||||
@@ -83,95 +144,95 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
_client = DAVClient(dav);
|
||||
final pingFuture = _client!.pingCompleter.future;
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.account),
|
||||
ListHeader(title: appLocalizations.local),
|
||||
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: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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(),
|
||||
);
|
||||
onTap: () {
|
||||
_backupOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.localBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.localRecoveryDesc),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -180,6 +241,50 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -238,7 +343,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: uriController,
|
||||
maxLines: 2,
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.link),
|
||||
@@ -313,47 +418,3 @@ 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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ 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,
|
||||
@@ -62,7 +61,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.logLevel = value;
|
||||
appController.updateClashConfigDebounce();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
@@ -100,7 +98,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
onChanged: (String? value) {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.globalRealUa = value;
|
||||
appController.updateClashConfigDebounce();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
@@ -125,6 +122,34 @@ 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(
|
||||
@@ -141,9 +166,9 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
return generateSection(
|
||||
title: appLocalizations.app,
|
||||
items: [
|
||||
if (Platform.isAndroid)
|
||||
if (Platform.isAndroid) ...[
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.allowBypass,
|
||||
selector: (_, config) => config.vpnProps.allowBypass,
|
||||
builder: (_, allowBypass, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.arrow_forward_outlined),
|
||||
@@ -152,16 +177,18 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
delegate: SwitchDelegate(
|
||||
value: allowBypass,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.allowBypass = value;
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
allowBypass: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.systemProxy,
|
||||
selector: (_, config) => config.vpnProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.settings_ethernet),
|
||||
@@ -170,31 +197,69 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.systemProxy = value;
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
systemProxy: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.isCompatible,
|
||||
builder: (_, isCompatible, __) {
|
||||
selector: (_, config) => config.isCloseConnections,
|
||||
builder: (_, isCloseConnections, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.expand_outlined),
|
||||
title: Text(appLocalizations.compatible),
|
||||
subtitle: Text(appLocalizations.compatibleDesc),
|
||||
leading: const Icon(Icons.auto_delete_outlined),
|
||||
title: Text(appLocalizations.autoCloseConnections),
|
||||
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isCompatible,
|
||||
value: isCloseConnections,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.isCompatible = value;
|
||||
await appController.applyProfile();
|
||||
appController.config.isCloseConnections = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.onlyProxy,
|
||||
builder: (_, onlyProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.data_usage_outlined),
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: onlyProxy,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.onlyProxy = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Selector<Config, bool>(
|
||||
// selector: (_, config) => config.isCompatible,
|
||||
// builder: (_, isCompatible, __) {
|
||||
// return ListItem.switchItem(
|
||||
// leading: const Icon(Icons.expand_outlined),
|
||||
// title: Text(appLocalizations.compatible),
|
||||
// subtitle: Text(appLocalizations.compatibleDesc),
|
||||
// delegate: SwitchDelegate(
|
||||
// value: isCompatible,
|
||||
// onChanged: (bool value) async {
|
||||
// final appController = globalState.appController;
|
||||
// appController.config.isCompatible = value;
|
||||
// await appController.applyProfile();
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -229,6 +294,19 @@ 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, __) {
|
||||
@@ -275,7 +353,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.ipv6 = value;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -293,7 +370,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = context.read<ClashConfig>();
|
||||
clashConfig.allowLan = value;
|
||||
globalState.appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -311,7 +387,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.unifiedDelay = value;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -331,7 +406,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.findProcessMode =
|
||||
value ? FindProcessMode.always : FindProcessMode.off;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -349,7 +423,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.tcpConcurrent = value;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -370,7 +443,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
appController.clashConfig.geodataLoader = value
|
||||
? geodataLoaderMemconservative
|
||||
: geodataLoaderStandard;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -390,7 +462,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.externalController =
|
||||
value ? defaultExternalController : '';
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -417,7 +488,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = context.read<ClashConfig>();
|
||||
clashConfig.tun = Tun(enable: value);
|
||||
globalState.appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -555,3 +625,65 @@ 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),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,9 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
||||
timer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(timer) {
|
||||
connectionsNotifier.value = connectionsNotifier.value
|
||||
.copyWith(connections: clashCore.getConnections());
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
connections: clashCore.getConnections(),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -61,6 +62,18 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
clashCore.closeConnections();
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
connections: clashCore.getConnections(),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete_sweep_outlined),
|
||||
),
|
||||
];
|
||||
},
|
||||
);
|
||||
@@ -87,7 +100,7 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
||||
}
|
||||
|
||||
_handleBlockConnection(String id) {
|
||||
clashCore.closeConnections(id);
|
||||
clashCore.closeConnection(id);
|
||||
connectionsNotifier.value = connectionsNotifier.value
|
||||
.copyWith(connections: clashCore.getConnections());
|
||||
}
|
||||
@@ -227,7 +240,7 @@ class ConnectionsSearchDelegate extends SearchDelegate {
|
||||
}
|
||||
|
||||
_handleBlockConnection(String id) {
|
||||
clashCore.closeConnections(id);
|
||||
clashCore.closeConnection(id);
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
connections: clashCore.getConnections(),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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';
|
||||
@@ -28,34 +31,51 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16).copyWith(
|
||||
bottom: 88,
|
||||
),
|
||||
child: Selector<AppState, double>(
|
||||
selector: (_, appState) => appState.viewWidth,
|
||||
builder: (_, viewWidth, ___) {
|
||||
// final viewMode = other.getViewMode(viewWidth);
|
||||
// final isDesktop = viewMode == ViewMode.desktop;
|
||||
final columns = max(4 * ((viewWidth / 350).ceil()), 8);
|
||||
final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4;
|
||||
return Grid(
|
||||
crossAxisCount: max(4 * ((viewWidth / 320).ceil()), 8),
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
children: const [
|
||||
GridItem(
|
||||
children: [
|
||||
const GridItem(
|
||||
crossAxisCellCount: 8,
|
||||
child: NetworkSpeed(),
|
||||
),
|
||||
GridItem(
|
||||
if (Platform.isAndroid)
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
child: const VPNSwitch(),
|
||||
),
|
||||
if (system.isDesktop) ...[
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
child: const TUNSwitch(),
|
||||
),
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
child: const ProxySwitch(),
|
||||
),
|
||||
],
|
||||
const GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: OutboundMode(),
|
||||
),
|
||||
GridItem(
|
||||
const GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: NetworkDetection(),
|
||||
),
|
||||
GridItem(
|
||||
const GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: TrafficUsage(),
|
||||
),
|
||||
GridItem(
|
||||
const GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: IntranetIP(),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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';
|
||||
@@ -15,51 +14,53 @@ class NetworkDetection extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
final ipInfoNotifier = ValueNotifier<IpInfo?>(null);
|
||||
final timeoutNotifier = ValueNotifier<bool>(false);
|
||||
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||
const NetworkDetectionState(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
),
|
||||
);
|
||||
bool? _preIsStart;
|
||||
CancelToken? cancelToken;
|
||||
Function? _checkIpDebounce;
|
||||
CancelToken? cancelToken;
|
||||
|
||||
_checkIp(
|
||||
bool isInit,
|
||||
bool isStart,
|
||||
) async {
|
||||
_checkIp() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final isInit = appState.isInit;
|
||||
if (!isInit) return;
|
||||
timeoutNotifier.value = false;
|
||||
final isStart = appState.isStart;
|
||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
_preIsStart = isStart;
|
||||
if (cancelToken != null) {
|
||||
cancelToken!.cancel();
|
||||
cancelToken = null;
|
||||
}
|
||||
ipInfoNotifier.value = null;
|
||||
final ipInfo = await request.checkIp(cancelToken);
|
||||
if (ipInfo == null) {
|
||||
timeoutNotifier.value = true;
|
||||
return;
|
||||
} else {
|
||||
timeoutNotifier.value = false;
|
||||
cancelToken = CancelToken();
|
||||
try {
|
||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
);
|
||||
} catch (_) {
|
||||
|
||||
}
|
||||
_preIsStart = isStart;
|
||||
ipInfoNotifier.value = ipInfo;
|
||||
}
|
||||
|
||||
_checkIpContainer(Widget child) {
|
||||
_checkIpDebounce = debounce(_checkIp);
|
||||
return Selector2<AppState, Config, CheckIpSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
return CheckIpSelectorState(
|
||||
isInit: appState.isInit,
|
||||
selectedMap: appState.selectedMap,
|
||||
isStart: appState.isStart,
|
||||
checkIpNum: appState.checkIpNum,
|
||||
);
|
||||
return Selector<AppState, num>(
|
||||
selector: (_, appState) {
|
||||
return appState.checkIpNum;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
builder: (_, checkIpNum, child) {
|
||||
if (_checkIpDebounce != null) {
|
||||
_checkIpDebounce!([state.isInit, state.isStart]);
|
||||
_checkIpDebounce!();
|
||||
}
|
||||
return child;
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
@@ -68,16 +69,28 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
ipInfoNotifier.dispose();
|
||||
timeoutNotifier.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);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_checkIpDebounce ??= debounce(_checkIp);
|
||||
return _checkIpContainer(
|
||||
ValueListenableBuilder<IpInfo?>(
|
||||
valueListenable: ipInfoNotifier,
|
||||
builder: (_, ipInfo, __) {
|
||||
ValueListenableBuilder<NetworkDetectionState>(
|
||||
valueListenable: networkDetectionState,
|
||||
builder: (_, state, __) {
|
||||
final ipInfo = state.ipInfo;
|
||||
final isTesting = state.isTesting;
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Column(
|
||||
@@ -98,37 +111,38 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: FadeBox(
|
||||
child: ipInfo != null
|
||||
? CountryFlag.fromCountryCode(
|
||||
ipInfo.countryCode,
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: isTesting
|
||||
? Text(
|
||||
appLocalizations.checking,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium,
|
||||
)
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: timeoutNotifier,
|
||||
builder: (_, timeout, __) {
|
||||
if (timeout) {
|
||||
return Text(
|
||||
appLocalizations.checkError,
|
||||
: ipInfo != null
|
||||
? Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: globalState.appController
|
||||
.measure.titleMediumHeight,
|
||||
child: Text(
|
||||
countryCodeToEmoji(
|
||||
ipInfo.countryCode),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
return TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.checking,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
fontFamily: "Twemoji",
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
appLocalizations.checkError,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -161,28 +175,24 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
),
|
||||
],
|
||||
)
|
||||
: 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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
: 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -114,7 +114,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.networkSpeed,
|
||||
iconData: Icons.speed,
|
||||
iconData: Icons.speed_sharp,
|
||||
),
|
||||
child: Selector<AppState, List<Traffic>>(
|
||||
selector: (_, appState) => appState.traffics,
|
||||
|
||||
@@ -15,7 +15,6 @@ class OutboundMode extends StatelessWidget {
|
||||
final clashConfig = appController.clashConfig;
|
||||
if (value == null || clashConfig.mode == value) return;
|
||||
clashConfig.mode = value;
|
||||
await appController.updateClashConfig();
|
||||
appController.addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
@@ -28,7 +27,7 @@ class OutboundMode extends StatelessWidget {
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.outboundMode,
|
||||
iconData: Icons.call_split,
|
||||
iconData: Icons.call_split_sharp,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
|
||||
@@ -37,7 +37,7 @@ class _StartButtonState extends State<StartButton>
|
||||
if (isStart == appController.appState.isStart) {
|
||||
isStart = !isStart;
|
||||
updateController();
|
||||
appController.updateSystemProxy(isStart);
|
||||
appController.updateStatus(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();
|
||||
}
|
||||
|
||||
121
lib/fragments/dashboard/status_switch.dart
Normal file
121
lib/fragments/dashboard/status_switch.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'package:fl_clash/common/app_localizations.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VPNSwitch extends StatelessWidget {
|
||||
const VPNSwitch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchContainer(
|
||||
info: const Info(
|
||||
label: "VPN",
|
||||
iconData: Icons.stacked_line_chart,
|
||||
),
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.enable,
|
||||
builder: (_, enable, __) {
|
||||
return Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: enable,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.vpnProps = config.vpnProps.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TUNSwitch extends StatelessWidget {
|
||||
const TUNSwitch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchContainer(
|
||||
info: Info(
|
||||
label: appLocalizations.tun,
|
||||
iconData: Icons.stacked_line_chart,
|
||||
),
|
||||
child: Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||
builder: (_, enable, __) {
|
||||
return Switch(
|
||||
value: enable,
|
||||
onChanged: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProxySwitch extends StatelessWidget {
|
||||
const ProxySwitch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchContainer(
|
||||
info: Info(
|
||||
label: appLocalizations.systemProxy,
|
||||
iconData: Icons.shuffle,
|
||||
),
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.desktopProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: systemProxy,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.desktopProps =
|
||||
config.desktopProps.copyWith(systemProxy: value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SwitchContainer extends StatelessWidget {
|
||||
final Info info;
|
||||
final Widget child;
|
||||
|
||||
const SwitchContainer({
|
||||
super.key,
|
||||
required this.info,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoHeader(
|
||||
info: info,
|
||||
actions: [
|
||||
child,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class EditProfile extends StatefulWidget {
|
||||
final Profile profile;
|
||||
@@ -26,6 +31,8 @@ class _EditProfileState extends State<EditProfile> {
|
||||
late TextEditingController autoUpdateDurationController;
|
||||
late bool autoUpdate;
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
||||
Uint8List? fileData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -36,12 +43,16 @@ class _EditProfileState extends State<EditProfile> {
|
||||
autoUpdateDurationController = TextEditingController(
|
||||
text: widget.profile.autoUpdateDuration.inMinutes.toString(),
|
||||
);
|
||||
appPath.getProfilePath(widget.profile.id).then((path) async {
|
||||
if (path == null) return;
|
||||
fileInfoNotifier.value = await _getFileInfo(path);
|
||||
});
|
||||
}
|
||||
|
||||
_handleConfirm() {
|
||||
_handleConfirm() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
final config = widget.context.read<Config>();
|
||||
final profile = widget.profile.copyWith(
|
||||
var profile = widget.profile.copyWith(
|
||||
url: urlController.text,
|
||||
label: labelController.text,
|
||||
autoUpdate: autoUpdate,
|
||||
@@ -52,7 +63,11 @@ class _EditProfileState extends State<EditProfile> {
|
||||
),
|
||||
);
|
||||
final hasUpdate = widget.profile.url != profile.url;
|
||||
config.setProfile(profile);
|
||||
if (fileData != null) {
|
||||
config.setProfile(await profile.saveFile(fileData!));
|
||||
} else {
|
||||
config.setProfile(profile);
|
||||
}
|
||||
if (hasUpdate) {
|
||||
globalState.homeScaffoldKey.currentState?.loadingRun(
|
||||
() async {
|
||||
@@ -62,7 +77,9 @@ class _EditProfileState extends State<EditProfile> {
|
||||
},
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
_setAutoUpdate(bool value) {
|
||||
@@ -72,6 +89,47 @@ class _EditProfileState extends State<EditProfile> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<FileInfo?> _getFileInfo(path) async {
|
||||
final file = File(path);
|
||||
if (!await file.exists()) {
|
||||
return null;
|
||||
}
|
||||
final lastModified = await file.lastModified();
|
||||
final size = await file.length();
|
||||
return FileInfo(
|
||||
size: size,
|
||||
lastModified: lastModified,
|
||||
);
|
||||
}
|
||||
|
||||
_editProfileFile() async {
|
||||
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
||||
if (profilePath == null) return;
|
||||
globalState.safeRun(() async {
|
||||
if (Platform.isAndroid) {
|
||||
await app?.openFile(
|
||||
profilePath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await launchUrl(
|
||||
Uri.file(
|
||||
profilePath,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_uploadProfileFile() async {
|
||||
final platformFile = await globalState.safeRun(picker.pickerFile);
|
||||
if (platformFile?.bytes == null) return;
|
||||
fileData = platformFile?.bytes;
|
||||
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
|
||||
size: fileData?.length ?? 0,
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = [
|
||||
@@ -141,7 +199,51 @@ class _EditProfileState extends State<EditProfile> {
|
||||
},
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
ValueListenableBuilder<FileInfo?>(
|
||||
valueListenable: fileInfoNotifier,
|
||||
builder: (_, fileInfo, __) {
|
||||
return FadeBox(
|
||||
child: fileInfo == null
|
||||
? Container()
|
||||
: ListItem(
|
||||
title: Text(
|
||||
appLocalizations.profile,
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
fileInfo.desc,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 6,
|
||||
spacing: 12,
|
||||
children: [
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.edit),
|
||||
label: appLocalizations.edit,
|
||||
onPressed: _editProfileFile,
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.upload),
|
||||
label: appLocalizations.upload,
|
||||
onPressed: _uploadProfileFile,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
return FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
@@ -159,7 +261,9 @@ class _EditProfileState extends State<EditProfile> {
|
||||
vertical: 16,
|
||||
),
|
||||
child: ListView.separated(
|
||||
primary: true,
|
||||
padding: kMaterialListPadding.copyWith(
|
||||
bottom: 72,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
|
||||
import 'package:fl_clash/fragments/profiles/view_profile.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -16,7 +17,6 @@ enum ProfileActions {
|
||||
edit,
|
||||
update,
|
||||
delete,
|
||||
view,
|
||||
}
|
||||
|
||||
class ProfilesFragment extends StatefulWidget {
|
||||
@@ -27,11 +27,8 @@ class ProfilesFragment extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
final hasPadding = ValueNotifier<bool>(false);
|
||||
Function? applyConfigDebounce;
|
||||
|
||||
List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = [];
|
||||
|
||||
_handleShowAddExtendPage() {
|
||||
showExtendPage(
|
||||
globalState.navigatorKey.currentState!.context,
|
||||
@@ -42,29 +39,52 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
);
|
||||
}
|
||||
|
||||
_getColumns(ViewMode viewMode) {
|
||||
switch (viewMode) {
|
||||
case ViewMode.mobile:
|
||||
return 1;
|
||||
case ViewMode.laptop:
|
||||
return 1;
|
||||
case ViewMode.desktop:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
_updateProfiles() async {
|
||||
final updateProfiles = profileItemKeys.map<Future>(
|
||||
(key) async => await key.currentState?.updateProfile(false));
|
||||
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)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_initScaffoldState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
if (!mounted) return;
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
if (!context.mounted) return;
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
@@ -72,35 +92,29 @@ 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,
|
||||
),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
hasPadding.dispose();
|
||||
}
|
||||
|
||||
_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(
|
||||
@@ -125,7 +139,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
selector: (_, appState, config) => ProfilesSelectorState(
|
||||
profiles: config.profiles,
|
||||
currentProfileId: config.currentProfileId,
|
||||
viewMode: appState.viewMode,
|
||||
columns: other.getProfilesColumns(appState.viewWidth),
|
||||
),
|
||||
builder: (context, state, child) {
|
||||
if (state.profiles.isEmpty) {
|
||||
@@ -133,47 +147,30 @@ 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: NotificationListener<ScrollNotification>(
|
||||
onNotification: (scrollNotification) {
|
||||
hasPadding.value =
|
||||
scrollNotification.metrics.maxScrollExtent > 0;
|
||||
return true;
|
||||
},
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: hasPadding,
|
||||
builder: (_, hasPadding, __) {
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 16 + (hasPadding ? 72 : 0),
|
||||
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: 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -184,7 +181,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileItem extends StatefulWidget {
|
||||
class ProfileItem extends StatelessWidget {
|
||||
final Profile profile;
|
||||
final String? groupValue;
|
||||
final void Function(String? value) onChanged;
|
||||
@@ -196,296 +193,330 @@ class ProfileItem extends StatefulWidget {
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProfileItem> createState() => _ProfileItemState();
|
||||
}
|
||||
|
||||
class _ProfileItemState extends State<ProfileItem> {
|
||||
final isUpdating = ValueNotifier<bool>(false);
|
||||
|
||||
_handleDeleteProfile() async {
|
||||
globalState.appController.deleteProfile(widget.profile.id);
|
||||
_handleDeleteProfile(BuildContext context) async {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.deleteProfileTip,
|
||||
),
|
||||
onTab: () async {
|
||||
await globalState.appController.deleteProfile(profile.id);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_handleUpdateProfile() async {
|
||||
await globalState.safeRun<void>(updateProfile);
|
||||
}
|
||||
|
||||
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 &&
|
||||
!appController.appState.isStart) {
|
||||
globalState.appController.rawApplyProfile();
|
||||
}
|
||||
} catch (e) {
|
||||
isUpdating.value = false;
|
||||
if (!isSingle) {
|
||||
return e.toString();
|
||||
} else {
|
||||
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,
|
||||
),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
isUpdating.value = false;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
_handleShowEditExtendPage() {
|
||||
_handleShowEditExtendPage(BuildContext context) {
|
||||
showExtendPage(
|
||||
context,
|
||||
body: EditProfile(
|
||||
profile: widget.profile,
|
||||
profile: profile,
|
||||
context: context,
|
||||
),
|
||||
title: "${appLocalizations.edit}${appLocalizations.profile}",
|
||||
);
|
||||
}
|
||||
|
||||
_handleViewProfile() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ViewProfile(
|
||||
profile: widget.profile,
|
||||
),
|
||||
List<Widget> _buildUserInfo(BuildContext context, UserInfo userInfo) {
|
||||
final use = userInfo.upload + userInfo.download;
|
||||
final total = userInfo.total;
|
||||
if (total == 0) {
|
||||
return [];
|
||||
}
|
||||
final useShow = TrafficValue(value: use).show;
|
||||
final totalShow = TrafficValue(value: total).show;
|
||||
final progress = total == 0 ? 0.0 : use / total;
|
||||
final expireShow = userInfo.expire == 0
|
||||
? appLocalizations.infiniteTime
|
||||
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
|
||||
return [
|
||||
LinearProgressIndicator(
|
||||
minHeight: 6,
|
||||
value: progress,
|
||||
backgroundColor: context.colorScheme.primary.toSoft(),
|
||||
),
|
||||
);
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Text(
|
||||
"$useShow / $totalShow · $expireShow",
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
_buildTitle(Profile profile) {
|
||||
final textTheme = context.textTheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
profile.label ?? profile.id,
|
||||
style: textTheme.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
|
||||
style: textTheme.labelMedium?.toLight,
|
||||
),
|
||||
],
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final userInfo = profile.userInfo ?? const UserInfo();
|
||||
final use = userInfo.upload + userInfo.download;
|
||||
final total = userInfo.total;
|
||||
final useShow = TrafficValue(value: use).show;
|
||||
final totalShow = TrafficValue(value: total).show;
|
||||
final progress = total == 0 ? 0.0 : use / total;
|
||||
final expireShow = userInfo.expire == 0
|
||||
? appLocalizations.infiniteTime
|
||||
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000)
|
||||
.show;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 6,
|
||||
value: progress,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$useShow / $totalShow",
|
||||
style: textTheme.labelMedium?.toLight,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
appLocalizations.expirationTime,
|
||||
style: textTheme.labelMedium?.toLighter,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
expireShow,
|
||||
style: textTheme.labelMedium?.toLighter,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
// final child = switch (userInfo != null) {
|
||||
// true => () {
|
||||
// final use = userInfo!.upload + userInfo.download;
|
||||
// final total = userInfo.total;
|
||||
// final useShow = TrafficValue(value: use).show;
|
||||
// final totalShow = TrafficValue(value: total).show;
|
||||
// final progress = total == 0 ? 0.0 : use / total;
|
||||
// final expireShow = userInfo.expire == 0
|
||||
// ? appLocalizations.infiniteTime
|
||||
// : DateTime.fromMillisecondsSinceEpoch(
|
||||
// userInfo.expire * 1000)
|
||||
// .show;
|
||||
// return Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Container(
|
||||
// margin: const EdgeInsets.symmetric(
|
||||
// vertical: 8,
|
||||
// ),
|
||||
// child: LinearProgressIndicator(
|
||||
// minHeight: 6,
|
||||
// value: progress,
|
||||
// ),
|
||||
// ),
|
||||
// Text(
|
||||
// "$useShow / $totalShow",
|
||||
// style: textTheme.labelMedium?.toLight(),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 2,
|
||||
// ),
|
||||
// Row(
|
||||
// children: [
|
||||
// Text(
|
||||
// appLocalizations.expirationTime,
|
||||
// style: textTheme.labelMedium?.toLighter(),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// width: 4,
|
||||
// ),
|
||||
// Text(
|
||||
// expireShow,
|
||||
// style: textTheme.labelMedium?.toLighter(),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// );
|
||||
// }(),
|
||||
// false => Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(top: 8),
|
||||
// child: CommonChip(
|
||||
// onPressed: _handleViewProfile,
|
||||
// avatar: const Icon(Icons.remove_red_eye),
|
||||
// label: appLocalizations.view,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// };
|
||||
// final measure = globalState.appController.measure;
|
||||
// final height = 6 + 8 * 2 + 2 + measure.labelMediumHeight * 2;
|
||||
// return SizedBox(
|
||||
// height: height,
|
||||
// child: child,
|
||||
// );
|
||||
}),
|
||||
],
|
||||
List<Widget> _buildUrlProfileInfo(BuildContext context) {
|
||||
final userInfo = profile.userInfo;
|
||||
return [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
);
|
||||
if (userInfo != null) ..._buildUserInfo(context, userInfo),
|
||||
Text(
|
||||
profile.lastUpdateDate?.lastUpdateTimeDesc ?? "",
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isUpdating.dispose();
|
||||
super.dispose();
|
||||
List<Widget> _buildFileProfileInfo(BuildContext context) {
|
||||
return [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Text(
|
||||
profile.lastUpdateDate?.lastUpdateTimeDesc ?? "",
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profile = widget.profile;
|
||||
final groupValue = widget.groupValue;
|
||||
final onChanged = widget.onChanged;
|
||||
return CommonCard(
|
||||
child: ListItem.radio(
|
||||
isSelected: profile.id == groupValue,
|
||||
onPressed: () {
|
||||
onChanged(profile.id);
|
||||
},
|
||||
child: ListItem(
|
||||
key: Key(profile.id),
|
||||
horizontalTitleGap: 16,
|
||||
delegate: RadioDelegate<String?>(
|
||||
value: profile.id,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
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.view,
|
||||
label: appLocalizations.view,
|
||||
iconData: Icons.visibility,
|
||||
),
|
||||
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 ProfileActions.view:
|
||||
_handleViewProfile();
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
));
|
||||
},
|
||||
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,
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ 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/intellij-light.dart';
|
||||
import 'package:re_highlight/styles/atom-one-light.dart';
|
||||
|
||||
class ViewProfile extends StatefulWidget {
|
||||
final Profile profile;
|
||||
@@ -23,29 +23,27 @@ class ViewProfile extends StatefulWidget {
|
||||
|
||||
class _ViewProfileState extends State<ViewProfile> {
|
||||
bool readOnly = true;
|
||||
CodeLineEditingController? controller;
|
||||
final contentNotifier = ValueNotifier<String>("");
|
||||
final CodeLineEditingController _controller = CodeLineEditingController();
|
||||
final key = GlobalKey<CommonScaffoldState>();
|
||||
final _focusNode = FocusNode();
|
||||
String? rawText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
||||
if (profilePath == null) {
|
||||
return;
|
||||
}
|
||||
final file = File(profilePath);
|
||||
final text = await file.readAsString();
|
||||
contentNotifier.value = text;
|
||||
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();
|
||||
contentNotifier.dispose();
|
||||
controller?.dispose();
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
}
|
||||
|
||||
Profile get profile => widget.profile;
|
||||
@@ -56,16 +54,9 @@ class _ViewProfileState extends State<ViewProfile> {
|
||||
readOnly = false;
|
||||
});
|
||||
} else {
|
||||
final text = controller?.text;
|
||||
if (text == null || text == contentNotifier.value) {
|
||||
setState(() {
|
||||
readOnly = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
contentNotifier.value = text;
|
||||
if (_controller.text == rawText) return;
|
||||
final newProfile = await key.currentState?.loadingRun<Profile>(() async {
|
||||
return await profile.saveFileWithString(text);
|
||||
return await profile.saveFileWithString(_controller.text);
|
||||
});
|
||||
if (newProfile == null) return;
|
||||
globalState.appController.config.setProfile(newProfile);
|
||||
@@ -81,74 +72,67 @@ class _ViewProfileState extends State<ViewProfile> {
|
||||
key: key,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: controller?.undo,
|
||||
onPressed: _controller.undo,
|
||||
icon: const Icon(Icons.undo),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: controller?.redo,
|
||||
onPressed: _controller.redo,
|
||||
icon: const Icon(Icons.redo),
|
||||
),
|
||||
if (!widget.profile.realAutoUpdate)
|
||||
IconButton(
|
||||
onPressed: _handleChangeReadOnly,
|
||||
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
IconButton(
|
||||
onPressed: _handleChangeReadOnly,
|
||||
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
|
||||
),
|
||||
],
|
||||
body: ValueListenableBuilder(
|
||||
valueListenable: contentNotifier,
|
||||
builder: (_, value, __) {
|
||||
if (value.isEmpty) return Container();
|
||||
controller = CodeLineEditingController.fromText(value);
|
||||
return CodeEditor(
|
||||
autofocus: false,
|
||||
readOnly: readOnly,
|
||||
scrollbarBuilder: (context, child, details) {
|
||||
return Scrollbar(
|
||||
controller: details.controller,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(2),
|
||||
interactive: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
showCursorWhenReadOnly: false,
|
||||
controller: controller,
|
||||
toolbarController:
|
||||
!readOnly ? const ContextMenuControllerImpl() : null,
|
||||
shortcutsActivatorsBuilder:
|
||||
const DefaultCodeShortcutsActivatorsBuilder(),
|
||||
indicatorBuilder:
|
||||
(context, editingController, chunkController, notifier) {
|
||||
return Row(
|
||||
children: [
|
||||
DefaultCodeLineNumber(
|
||||
controller: editingController,
|
||||
notifier: notifier,
|
||||
),
|
||||
DefaultCodeChunkIndicator(
|
||||
width: 20,
|
||||
controller: chunkController,
|
||||
notifier: notifier,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
style: CodeEditorStyle(
|
||||
fontSize: 14,
|
||||
codeTheme: CodeHighlightTheme(
|
||||
languages: {
|
||||
'yaml': CodeHighlightThemeMode(
|
||||
mode: langYaml,
|
||||
)
|
||||
},
|
||||
theme: intellijLightTheme,
|
||||
),
|
||||
),
|
||||
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,
|
||||
);
|
||||
@@ -164,10 +148,38 @@ class ContextMenuItemWidget extends PopupMenuItem<void> {
|
||||
}
|
||||
|
||||
class ContextMenuControllerImpl implements SelectionToolbarController {
|
||||
const ContextMenuControllerImpl();
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
ContextMenuControllerImpl(
|
||||
this.focusNode,
|
||||
);
|
||||
|
||||
_removeOverLayEntry() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void hide(BuildContext context) {}
|
||||
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({
|
||||
@@ -181,27 +193,40 @@ class ContextMenuControllerImpl implements SelectionToolbarController {
|
||||
if (controller.selectedText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromSize(
|
||||
(anchors.secondaryAnchor ?? anchors.primaryAnchor) &
|
||||
const Size(150, double.infinity),
|
||||
MediaQuery.of(context).size,
|
||||
),
|
||||
items: [
|
||||
ContextMenuItemWidget(
|
||||
text: appLocalizations.cut,
|
||||
onTap: controller.cut,
|
||||
),
|
||||
ContextMenuItemWidget(
|
||||
text: appLocalizations.copy,
|
||||
onTap: controller.copy,
|
||||
),
|
||||
ContextMenuItemWidget(
|
||||
text: appLocalizations.paste,
|
||||
onTap: controller.paste,
|
||||
),
|
||||
],
|
||||
_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!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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/fragments/proxies/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
@@ -10,7 +10,7 @@ import 'package:provider/provider.dart';
|
||||
class ProxyCard extends StatelessWidget {
|
||||
final String groupName;
|
||||
final Proxy proxy;
|
||||
final bool isSelected;
|
||||
final GroupType groupType;
|
||||
final CommonCardType style;
|
||||
final ProxyCardType type;
|
||||
|
||||
@@ -18,7 +18,7 @@ class ProxyCard extends StatelessWidget {
|
||||
super.key,
|
||||
required this.groupName,
|
||||
required this.proxy,
|
||||
required this.isSelected,
|
||||
required this.groupType,
|
||||
this.style = CommonCardType.plain,
|
||||
required this.type,
|
||||
});
|
||||
@@ -69,47 +69,51 @@ class ProxyCard extends StatelessWidget {
|
||||
if (type == ProxyCardType.min) {
|
||||
return SizedBox(
|
||||
height: measure.bodyMediumHeight * 1,
|
||||
child: Text(
|
||||
child: EmojiText(
|
||||
proxy.name,
|
||||
maxLines: 1,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: measure.bodyMediumHeight * 2,
|
||||
child: Text(
|
||||
child: EmojiText(
|
||||
proxy.name,
|
||||
maxLines: 2,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_changeProxy(BuildContext context) {
|
||||
_changeProxy(BuildContext context) async {
|
||||
final appController = globalState.appController;
|
||||
final group = appController.appState.getGroupWithName(groupName)!;
|
||||
if (group.type != GroupType.Selector) {
|
||||
globalState.showSnackBar(
|
||||
context,
|
||||
message: appLocalizations.notSelectedTip,
|
||||
final isUrlTest = groupType == GroupType.URLTest;
|
||||
final isSelector = groupType == GroupType.Selector;
|
||||
if (isUrlTest || isSelector) {
|
||||
final currentProxyName =
|
||||
appController.config.currentSelectedMap[groupName];
|
||||
final nextProxyName = switch (isUrlTest) {
|
||||
true => currentProxyName == proxy.name ? "" : proxy.name,
|
||||
false => proxy.name,
|
||||
};
|
||||
appController.config.updateCurrentSelectedMap(
|
||||
groupName,
|
||||
nextProxyName,
|
||||
);
|
||||
appController.changeProxy(
|
||||
groupName: groupName,
|
||||
proxyName: nextProxyName,
|
||||
);
|
||||
await appController.updateGroupDebounce();
|
||||
return;
|
||||
}
|
||||
globalState.appController.config.updateCurrentSelectedMap(
|
||||
groupName,
|
||||
proxy.name,
|
||||
);
|
||||
clashCore.changeProxy(
|
||||
ChangeProxyParams(
|
||||
groupName: groupName,
|
||||
proxyName: proxy.name,
|
||||
),
|
||||
globalState.showSnackBar(
|
||||
context,
|
||||
message: appLocalizations.notSelectedTip,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,75 +122,123 @@ class ProxyCard extends StatelessWidget {
|
||||
final measure = globalState.appController.measure;
|
||||
final delayText = _buildDelayText();
|
||||
final proxyNameText = _buildProxyNameText(context);
|
||||
return CommonCard(
|
||||
type: style,
|
||||
key: key,
|
||||
onPressed: () {
|
||||
_changeProxy(context);
|
||||
},
|
||||
isSelected: isSelected,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return currentGroupProxyNameBuilder(
|
||||
groupName: groupName,
|
||||
builder: (currentGroupName) {
|
||||
return Stack(
|
||||
children: [
|
||||
proxyNameText,
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (type == ProxyCardType.expand) ...[
|
||||
SizedBox(
|
||||
height: measure.bodySmallHeight,
|
||||
child: Selector<AppState, String>(
|
||||
selector: (context, appState) => appState.getDesc(
|
||||
proxy.type,
|
||||
proxy.name,
|
||||
),
|
||||
builder: (_, desc, __) {
|
||||
return TooltipText(
|
||||
text: Text(
|
||||
desc,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: context.textTheme.bodySmall?.color?.toLight(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
delayText,
|
||||
] else
|
||||
SizedBox(
|
||||
height: measure.bodySmallHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
CommonCard(
|
||||
type: style,
|
||||
key: key,
|
||||
onPressed: () {
|
||||
_changeProxy(context);
|
||||
},
|
||||
isSelected: currentGroupName == proxy.name,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
proxy.type,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color:
|
||||
context.textTheme.bodySmall?.color?.toLight(),
|
||||
proxyNameText,
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (type == ProxyCardType.expand) ...[
|
||||
SizedBox(
|
||||
height: measure.bodySmallHeight,
|
||||
child: Selector<AppState, String>(
|
||||
selector: (context, appState) => appState.getDesc(
|
||||
proxy.type,
|
||||
proxy.name,
|
||||
),
|
||||
builder: (_, desc, __) {
|
||||
return EmojiText(
|
||||
desc,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.textTheme.bodySmall?.color
|
||||
?.toLight(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
delayText,
|
||||
] else
|
||||
SizedBox(
|
||||
height: measure.bodySmallHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
proxy.type,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: context.textTheme.bodySmall?.color
|
||||
?.toLight(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
delayText,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
delayText,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (groupType == GroupType.URLTest)
|
||||
Selector<Config, String>(
|
||||
selector: (_, config) {
|
||||
final selectedProxyName =
|
||||
config.currentSelectedMap[groupName];
|
||||
return selectedProxyName ?? '';
|
||||
},
|
||||
builder: (_, value, __) {
|
||||
if (value != proxy.name) return Container();
|
||||
return Positioned.fill(
|
||||
child: Container(
|
||||
alignment: Alignment.topRight,
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: const SelectIcon(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Positioned.fill(
|
||||
child: Container(
|
||||
alignment: Alignment.topRight,
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: const SelectIcon(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
76
lib/fragments/proxies/common.dart
Normal file
76
lib/fragments/proxies/common.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/other.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:provider/provider.dart';
|
||||
|
||||
Widget currentGroupProxyNameBuilder({
|
||||
required String groupName,
|
||||
required Widget Function(String currentGroupName) builder,
|
||||
}) {
|
||||
return Selector2<AppState, Config, String>(
|
||||
selector: (_, appState, config) {
|
||||
final group = appState.getGroupWithName(groupName);
|
||||
final selectedProxyName = config.currentSelectedMap[groupName];
|
||||
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
|
||||
},
|
||||
builder: (_, currentGroupName, ___) {
|
||||
return builder(currentGroupName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double get listHeaderHeight {
|
||||
final measure = globalState.appController.measure;
|
||||
return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight;
|
||||
}
|
||||
|
||||
double getItemHeight(ProxyCardType proxyCardType) {
|
||||
final measure = globalState.appController.measure;
|
||||
final baseHeight =
|
||||
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
|
||||
return switch (proxyCardType) {
|
||||
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
|
||||
ProxyCardType.shrink => baseHeight,
|
||||
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
|
||||
};
|
||||
}
|
||||
|
||||
delayTest(List<Proxy> proxies) async {
|
||||
final appController = globalState.appController;
|
||||
final delayProxies = proxies.map<Future>((proxy) async {
|
||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
});
|
||||
await Future.wait(delayProxies);
|
||||
appController.appState.sortNum++;
|
||||
}
|
||||
|
||||
double getScrollToSelectedOffset({
|
||||
required String groupName,
|
||||
required List<Proxy> proxies,
|
||||
}) {
|
||||
final appController = globalState.appController;
|
||||
final columns = other.getProxiesColumns(
|
||||
appController.appState.viewWidth,
|
||||
appController.config.proxiesLayout,
|
||||
);
|
||||
final proxyCardType = appController.config.proxyCardType;
|
||||
final selectedName = appController.getCurrentSelectedName(groupName);
|
||||
final findSelectedIndex = proxies.indexWhere(
|
||||
(proxy) => proxy.name == selectedName,
|
||||
);
|
||||
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
||||
final rows = (selectedIndex / columns).floor();
|
||||
return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0);
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
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:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'card.dart';
|
||||
|
||||
class ProxyGroupView extends StatefulWidget {
|
||||
final String groupName;
|
||||
final ProxiesType type;
|
||||
|
||||
const ProxyGroupView({
|
||||
super.key,
|
||||
required this.groupName,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProxyGroupView> createState() => _ProxyGroupViewState();
|
||||
}
|
||||
|
||||
class _ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
var isLock = false;
|
||||
final scrollController = ScrollController();
|
||||
var isEnd = false;
|
||||
|
||||
String get groupName => widget.groupName;
|
||||
|
||||
ProxiesType get type => widget.type;
|
||||
|
||||
double _getItemHeight(ProxyCardType proxyCardType) {
|
||||
final measure = globalState.appController.measure;
|
||||
final baseHeight =
|
||||
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
|
||||
return switch(proxyCardType){
|
||||
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
|
||||
ProxyCardType.shrink => baseHeight,
|
||||
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
|
||||
};
|
||||
}
|
||||
|
||||
_delayTest(List<Proxy> proxies) async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
final appController = globalState.appController;
|
||||
for (final proxy in proxies) {
|
||||
final proxyName =
|
||||
appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
clashCore.getDelay(proxyName).then((delay) {
|
||||
globalState.appController.setDelay(delay);
|
||||
});
|
||||
}
|
||||
await Future.delayed(httpTimeoutDuration + moreDuration);
|
||||
appController.appState.sortNum++;
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
Widget _currentProxyNameBuilder({
|
||||
required Widget Function(String) builder,
|
||||
}) {
|
||||
return Selector2<AppState, Config, String>(
|
||||
selector: (_, appState, config) {
|
||||
final group = appState.getGroupWithName(groupName)!;
|
||||
return config.currentSelectedMap[groupName] ?? group.now ?? '';
|
||||
},
|
||||
builder: (_, value, ___) {
|
||||
return builder(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabGroupView({
|
||||
required List<Proxy> proxies,
|
||||
required int columns,
|
||||
required ProxyCardType proxyCardType,
|
||||
}) {
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
proxies,
|
||||
);
|
||||
return DelayTestButtonContainer(
|
||||
onClick: () async {
|
||||
await _delayTest(
|
||||
proxies,
|
||||
);
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: _getItemHeight(proxyCardType),
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return _currentProxyNameBuilder(builder: (value) {
|
||||
return ProxyCard(
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
isSelected: value == proxy.name,
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpansionGroupView({
|
||||
required List<Proxy> proxies,
|
||||
required int columns,
|
||||
required ProxyCardType proxyCardType,
|
||||
}) {
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
proxies,
|
||||
);
|
||||
final group =
|
||||
globalState.appController.appState.getGroupWithName(groupName)!;
|
||||
final itemHeight = _getItemHeight(proxyCardType);
|
||||
final innerHeight = context.appSize.height - 200;
|
||||
final lines = (sortedProxies.length / columns).ceil();
|
||||
final minLines =
|
||||
innerHeight >= 200 ? (innerHeight / itemHeight).floor() : 3;
|
||||
final height = (itemHeight + 8) * min(lines, minLines) - 8;
|
||||
return Selector<Config, Set<String>>(
|
||||
selector: (_, config) => config.currentUnfoldSet,
|
||||
builder: (_, currentUnfoldSet, __) {
|
||||
return CommonCard(
|
||||
child: ExpansionTile(
|
||||
childrenPadding: const EdgeInsets.all(8),
|
||||
initiallyExpanded: currentUnfoldSet.contains(groupName),
|
||||
iconColor: context.colorScheme.onSurfaceVariant,
|
||||
onExpansionChanged: (value) {
|
||||
final tempUnfoldSet = Set<String>.from(currentUnfoldSet);
|
||||
if (value) {
|
||||
tempUnfoldSet.add(groupName);
|
||||
} else {
|
||||
tempUnfoldSet.remove(groupName);
|
||||
}
|
||||
globalState.appController.config.updateCurrentUnfoldSet(
|
||||
tempUnfoldSet,
|
||||
);
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(groupName),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
group.type.name,
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: _currentProxyNameBuilder(
|
||||
builder: (value) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (value.isNotEmpty) ...[
|
||||
Icon(
|
||||
Icons.arrow_right,
|
||||
color: context
|
||||
.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
value,
|
||||
style: context
|
||||
.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.network_ping,
|
||||
size: 20,
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () {
|
||||
_delayTest(sortedProxies);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
shape: const RoundedRectangleBorder(
|
||||
side: BorderSide.none,
|
||||
),
|
||||
collapsedShape: const RoundedRectangleBorder(
|
||||
side: BorderSide.none,
|
||||
),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: GridView.builder(
|
||||
key: widget.key,
|
||||
controller: scrollController,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: _getItemHeight(proxyCardType),
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return _currentProxyNameBuilder(
|
||||
builder: (value) {
|
||||
return ProxyCard(
|
||||
style: CommonCardType.filled,
|
||||
type: proxyCardType,
|
||||
isSelected: value == proxy.name,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
scrollController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, ProxyGroupSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final group = appState.getGroupWithName(groupName)!;
|
||||
return ProxyGroupSelectorState(
|
||||
proxyCardType: config.proxyCardType,
|
||||
proxiesSortType: config.proxiesSortType,
|
||||
columns: globalState.appController.columns,
|
||||
sortNum: appState.sortNum,
|
||||
proxies: group.all,
|
||||
);
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
final proxies = state.proxies;
|
||||
final columns = state.columns;
|
||||
final proxyCardType = state.proxyCardType;
|
||||
return switch (type) {
|
||||
ProxiesType.tab => _buildTabGroupView(
|
||||
proxies: proxies,
|
||||
columns: columns,
|
||||
proxyCardType: proxyCardType,
|
||||
),
|
||||
ProxiesType.list => _buildExpansionGroupView(
|
||||
proxies: proxies,
|
||||
columns: columns,
|
||||
proxyCardType: proxyCardType,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DelayTestButtonContainer extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Future Function() onClick;
|
||||
|
||||
const DelayTestButtonContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onClick,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DelayTestButtonContainer> createState() =>
|
||||
_DelayTestButtonContainerState();
|
||||
}
|
||||
|
||||
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scale;
|
||||
|
||||
_healthcheck() async {
|
||||
_controller.forward();
|
||||
await widget.onClick();
|
||||
_controller.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
);
|
||||
_scale = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(
|
||||
0,
|
||||
1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_controller.reverse();
|
||||
return FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
return SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: Transform.scale(
|
||||
scale: _scale.value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: _healthcheck,
|
||||
child: const Icon(Icons.network_ping),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,525 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/card.dart';
|
||||
import 'package:fl_clash/widgets/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'group.dart';
|
||||
import 'card.dart';
|
||||
import 'common.dart';
|
||||
|
||||
typedef GroupNameProxiesMap = Map<String, List<Proxy>>;
|
||||
|
||||
class ProxiesListFragment extends StatefulWidget {
|
||||
const ProxiesListFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ProxiesListFragment> createState() =>
|
||||
_ProxiesListFragmentState();
|
||||
State<ProxiesListFragment> createState() => _ProxiesListFragmentState();
|
||||
}
|
||||
|
||||
class _ProxiesListFragmentState
|
||||
extends State<ProxiesListFragment> {
|
||||
class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
final _controller = ScrollController();
|
||||
final _headerStateNotifier = ValueNotifier<ProxiesListHeaderSelectorState>(
|
||||
const ProxiesListHeaderSelectorState(
|
||||
offset: 0,
|
||||
currentIndex: 0,
|
||||
),
|
||||
);
|
||||
List<double> _headerOffset = [];
|
||||
GroupNameProxiesMap _lastGroupNameProxiesMap = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(_adjustHeader);
|
||||
}
|
||||
|
||||
_adjustHeader() {
|
||||
final offset = _controller.offset;
|
||||
final index = _headerOffset.findInterval(offset);
|
||||
final currentIndex = index;
|
||||
double headerOffset = 0.0;
|
||||
if (index + 1 <= _headerOffset.length - 1) {
|
||||
final endOffset = _headerOffset[index + 1];
|
||||
final startOffset = endOffset - listHeaderHeight - 8;
|
||||
if (offset > startOffset && offset < endOffset) {
|
||||
headerOffset = offset - startOffset;
|
||||
}
|
||||
}
|
||||
_headerStateNotifier.value = _headerStateNotifier.value.copyWith(
|
||||
currentIndex: currentIndex,
|
||||
offset: headerOffset,
|
||||
);
|
||||
}
|
||||
|
||||
double _getListItemHeight(Type type, ProxyCardType proxyCardType) {
|
||||
return switch (type) {
|
||||
const (SizedBox) => 8,
|
||||
const (ListHeader) => listHeaderHeight,
|
||||
Type() => getItemHeight(proxyCardType),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_headerStateNotifier.dispose();
|
||||
_controller.removeListener(_adjustHeader);
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
_handleChange(Set<String> currentUnfoldSet, String groupName) {
|
||||
final tempUnfoldSet = Set<String>.from(currentUnfoldSet);
|
||||
if (tempUnfoldSet.contains(groupName)) {
|
||||
tempUnfoldSet.remove(groupName);
|
||||
} else {
|
||||
tempUnfoldSet.add(groupName);
|
||||
}
|
||||
globalState.appController.config.updateCurrentUnfoldSet(
|
||||
tempUnfoldSet,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_adjustHeader();
|
||||
});
|
||||
}
|
||||
|
||||
List<double> _getItemHeightList(
|
||||
List<Widget> items,
|
||||
ProxyCardType proxyCardType,
|
||||
) {
|
||||
final itemHeightList = <double>[];
|
||||
List<double> headerOffset = [];
|
||||
double currentHeight = 0;
|
||||
for (final item in items) {
|
||||
if (item.runtimeType == ListHeader) {
|
||||
headerOffset.add(currentHeight);
|
||||
}
|
||||
final itemHeight = _getListItemHeight(item.runtimeType, proxyCardType);
|
||||
itemHeightList.add(itemHeight);
|
||||
currentHeight = currentHeight + itemHeight;
|
||||
}
|
||||
_headerOffset = headerOffset;
|
||||
return itemHeightList;
|
||||
}
|
||||
|
||||
List<Widget> _buildItems({
|
||||
required List<String> groupNames,
|
||||
required int columns,
|
||||
required Set<String> currentUnfoldSet,
|
||||
required ProxyCardType type,
|
||||
}) {
|
||||
final items = <Widget>[];
|
||||
final GroupNameProxiesMap groupNameProxiesMap = {};
|
||||
for (final groupName in groupNames) {
|
||||
final group =
|
||||
globalState.appController.appState.getGroupWithName(groupName)!;
|
||||
final isExpand = currentUnfoldSet.contains(groupName);
|
||||
items.addAll([
|
||||
ListHeader(
|
||||
onScrollToSelected: _scrollToGroupSelected,
|
||||
key: Key(groupName),
|
||||
isExpand: isExpand,
|
||||
group: group,
|
||||
onChange: (String groupName) {
|
||||
_handleChange(currentUnfoldSet, groupName);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
]);
|
||||
if (isExpand) {
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
group.all,
|
||||
);
|
||||
groupNameProxiesMap[groupName] = sortedProxies;
|
||||
final chunks = sortedProxies.chunks(columns);
|
||||
final rows = chunks.map<Widget>((proxies) {
|
||||
final children = proxies
|
||||
.map<Widget>(
|
||||
(proxy) => Flexible(
|
||||
child: ProxyCard(
|
||||
type: type,
|
||||
groupType: group.type,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
),
|
||||
),
|
||||
)
|
||||
.fill(
|
||||
columns,
|
||||
filler: (_) => const Flexible(
|
||||
child: SizedBox(),
|
||||
),
|
||||
)
|
||||
.separated(
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: children.toList(),
|
||||
);
|
||||
}).separated(
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
);
|
||||
items.addAll(
|
||||
[
|
||||
...rows,
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
_lastGroupNameProxiesMap = groupNameProxiesMap;
|
||||
return items;
|
||||
}
|
||||
|
||||
_buildHeader({
|
||||
required String groupName,
|
||||
required Set<String> currentUnfoldSet,
|
||||
}) {
|
||||
final group =
|
||||
globalState.appController.appState.getGroupWithName(groupName)!;
|
||||
final isExpand = currentUnfoldSet.contains(groupName);
|
||||
return SizedBox(
|
||||
height: listHeaderHeight,
|
||||
child: ListHeader(
|
||||
onScrollToSelected: _scrollToGroupSelected,
|
||||
key: Key(groupName),
|
||||
isExpand: isExpand,
|
||||
group: group,
|
||||
onChange: (String groupName) {
|
||||
_handleChange(currentUnfoldSet, groupName);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_scrollToGroupSelected(String groupName) {
|
||||
if (_controller.position.maxScrollExtent == 0) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
final currentGroups = appController.appState.currentGroups;
|
||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
||||
final findIndex = groupNames.indexWhere((item) => item == groupName);
|
||||
final index = findIndex != -1 ? findIndex : 0;
|
||||
final currentInitOffset = _headerOffset[index];
|
||||
final proxies = _lastGroupNameProxiesMap[groupName];
|
||||
_controller.animateTo(
|
||||
currentInitOffset +
|
||||
getScrollToSelectedOffset(
|
||||
groupName: groupName,
|
||||
proxies: proxies ?? [],
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, ProxiesSelectorState>(
|
||||
return Selector2<AppState, Config, ProxiesListSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final currentGroups = appState.currentGroups;
|
||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
||||
return ProxiesSelectorState(
|
||||
return ProxiesListSelectorState(
|
||||
groupNames: groupNames,
|
||||
currentGroupName: config.currentGroupName,
|
||||
currentUnfoldSet: config.currentUnfoldSet,
|
||||
proxyCardType: config.proxyCardType,
|
||||
proxiesSortType: config.proxiesSortType,
|
||||
columns: other.getProxiesColumns(
|
||||
appState.viewWidth,
|
||||
config.proxiesLayout,
|
||||
),
|
||||
sortNum: appState.sortNum,
|
||||
);
|
||||
},
|
||||
shouldRebuild: (prev, next) {
|
||||
if (!const ListEquality<String>()
|
||||
.equals(prev.groupNames, next.groupNames)) {
|
||||
_headerStateNotifier.value = const ProxiesListHeaderSelectorState(
|
||||
offset: 0,
|
||||
currentIndex: 0,
|
||||
);
|
||||
}
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: state.groupNames.length,
|
||||
itemBuilder: (_, index) {
|
||||
final groupName = state.groupNames[index];
|
||||
return ProxyGroupView(
|
||||
key: PageStorageKey(groupName),
|
||||
groupName: groupName,
|
||||
type: ProxiesType.list,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const SizedBox(
|
||||
height: 16,
|
||||
);
|
||||
},
|
||||
final items = _buildItems(
|
||||
groupNames: state.groupNames,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
columns: state.columns,
|
||||
type: state.proxyCardType,
|
||||
);
|
||||
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
|
||||
return Scrollbar(
|
||||
controller: _controller,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(8),
|
||||
interactive: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ScrollConfiguration(
|
||||
behavior: HiddenBarScrollBehavior(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
controller: _controller,
|
||||
itemExtentBuilder: (index, __) {
|
||||
return itemsOffset[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(builder: (_, container) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _headerStateNotifier,
|
||||
builder: (_, headerState, ___) {
|
||||
final index =
|
||||
headerState.currentIndex > state.groupNames.length - 1
|
||||
? 0
|
||||
: headerState.currentIndex;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -headerState.offset,
|
||||
child: Container(
|
||||
width: container.maxWidth,
|
||||
color: context.colorScheme.surface,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 8,
|
||||
),
|
||||
child: _buildHeader(
|
||||
groupName: state.groupNames[index],
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ListHeader extends StatefulWidget {
|
||||
final Group group;
|
||||
|
||||
final Function(String groupName) onChange;
|
||||
final Function(String groupName) onScrollToSelected;
|
||||
final bool isExpand;
|
||||
|
||||
const ListHeader({
|
||||
super.key,
|
||||
required this.group,
|
||||
required this.onChange,
|
||||
required this.onScrollToSelected,
|
||||
required this.isExpand,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ListHeader> createState() => _ListHeaderState();
|
||||
}
|
||||
|
||||
class _ListHeaderState extends State<ListHeader>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _iconTurns;
|
||||
var isLock = false;
|
||||
|
||||
String get groupName => widget.group.name;
|
||||
|
||||
String get groupType => widget.group.type.name;
|
||||
|
||||
bool get isExpand => widget.isExpand;
|
||||
|
||||
_delayTest(List<Proxy> proxies) async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
await delayTest(proxies);
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
_handleChange(String groupName) {
|
||||
if (isExpand) {
|
||||
_animationController.reverse();
|
||||
} else {
|
||||
_animationController.forward();
|
||||
}
|
||||
widget.onChange(groupName);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
_iconTurns = _animationController.drive(
|
||||
Tween<double>(begin: 0.0, end: 0.5),
|
||||
);
|
||||
if (isExpand) {
|
||||
_animationController.value = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ListHeader oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.isExpand != widget.isExpand) {
|
||||
if (isExpand) {
|
||||
_animationController.value = 1.0;
|
||||
} else {
|
||||
_animationController.value = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
key: widget.key,
|
||||
type: CommonCardType.filled,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
groupName,
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
groupType,
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: currentGroupProxyNameBuilder(
|
||||
groupName: groupName,
|
||||
builder: (currentGroupName) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (currentGroupName.isNotEmpty) ...[
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: EmojiText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
" · $currentGroupName",
|
||||
style: context
|
||||
.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (isExpand) ...[
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
widget.onScrollToSelected(groupName);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.adjust,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_delayTest(widget.group.all);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.network_ping,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
],
|
||||
AnimatedBuilder(
|
||||
animation: _animationController.view,
|
||||
builder: (_, __) {
|
||||
return IconButton.filledTonal(
|
||||
onPressed: () {
|
||||
_handleChange(groupName);
|
||||
},
|
||||
icon: RotationTransition(
|
||||
turns: _iconTurns,
|
||||
child: const Icon(
|
||||
Icons.expand_more,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
_handleChange(groupName);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
208
lib/fragments/proxies/providers.dart
Normal file
208
lib/fragments/proxies/providers.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
|
||||
@@ -17,12 +18,45 @@ class ProxiesFragment extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ProxiesFragmentState extends State<ProxiesFragment> {
|
||||
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
|
||||
|
||||
_initActions() {
|
||||
_initActions(ProxiesType proxiesType, bool hasProvider) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
],
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
@@ -43,23 +77,24 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.currentLabel == 'proxies',
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: Selector<Config, ProxiesType>(
|
||||
selector: (_, config) => config.proxiesType,
|
||||
builder: (_, proxiesType, __) {
|
||||
return switch (proxiesType) {
|
||||
ProxiesType.tab => const ProxiesTabFragment(),
|
||||
return Selector<Config, ProxiesType>(
|
||||
selector: (_, config) => config.proxiesType,
|
||||
builder: (_, proxiesType, __) {
|
||||
return ProxiesActionsBuilder(
|
||||
builder: (state, child) {
|
||||
if (state.isCurrent) {
|
||||
_initActions(proxiesType, state.hasProvider);
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: switch (proxiesType) {
|
||||
ProxiesType.tab => ProxiesTabFragment(
|
||||
key: _proxiesTabKey,
|
||||
),
|
||||
ProxiesType.list => const ProxiesListFragment(),
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,14 @@ class ProxiesSettingWidget extends StatelessWidget {
|
||||
};
|
||||
}
|
||||
|
||||
String getTextForProxiesLayout(ProxiesLayout proxiesLayout) {
|
||||
return switch (proxiesLayout) {
|
||||
ProxiesLayout.tight => appLocalizations.tight,
|
||||
ProxiesLayout.standard => appLocalizations.standard,
|
||||
ProxiesLayout.loose => appLocalizations.loose,
|
||||
};
|
||||
}
|
||||
|
||||
List<Widget> _buildStyleSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.style,
|
||||
@@ -132,36 +140,28 @@ class ProxiesSettingWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildColumnsSetting() {
|
||||
List<Widget> _buildLayoutSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.columns,
|
||||
title: appLocalizations.layout,
|
||||
items: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector2<AppState, Config, ColumnsSelectorState>(
|
||||
selector: (_, appState, config) => ColumnsSelectorState(
|
||||
columns: config.proxiesColumns,
|
||||
viewMode: appState.viewMode,
|
||||
),
|
||||
builder: (_, state, __) {
|
||||
child: Selector< Config, ProxiesLayout>(
|
||||
selector: (_, config) => config.proxiesLayout,
|
||||
builder: (_, proxiesLayout, __) {
|
||||
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 targetColumnsArray)
|
||||
for (final item in ProxiesLayout.values)
|
||||
SettingTextCard(
|
||||
other.getColumnsTextForInt(item),
|
||||
isSelected: item == currentColumns,
|
||||
getTextForProxiesLayout(item),
|
||||
isSelected: item == proxiesLayout,
|
||||
onPressed: () {
|
||||
config.proxiesColumns = item;
|
||||
config.proxiesLayout = item;
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -183,80 +183,10 @@ class ProxiesSettingWidget extends StatelessWidget {
|
||||
children: [
|
||||
..._buildStyleSetting(),
|
||||
..._buildSortSetting(),
|
||||
..._buildColumnsSetting(),
|
||||
..._buildLayoutSetting(),
|
||||
..._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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
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';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'group.dart';
|
||||
import 'card.dart';
|
||||
import 'common.dart';
|
||||
|
||||
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
|
||||
|
||||
class ProxiesTabFragment extends StatefulWidget {
|
||||
const ProxiesTabFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ProxiesTabFragment> createState() => _ProxiesTabFragmentState();
|
||||
State<ProxiesTabFragment> createState() => ProxiesTabFragmentState();
|
||||
}
|
||||
|
||||
class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
with TickerProviderStateMixin {
|
||||
TabController? _tabController;
|
||||
final hasMoreButtonNotifier = ValueNotifier<bool>(false);
|
||||
final _hasMoreButtonNotifier = ValueNotifier<bool>(false);
|
||||
GroupNameKeyMap _keyMap = {};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -28,6 +31,11 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
_tabController?.dispose();
|
||||
}
|
||||
|
||||
scrollToGroupSelected() {
|
||||
final currentGroupName = globalState.appController.config.currentGroupName;
|
||||
_keyMap[currentGroupName]?.currentState?.scrollToSelected();
|
||||
}
|
||||
|
||||
_buildMoreButton() {
|
||||
return Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.viewMode == ViewMode.mobile,
|
||||
@@ -83,6 +91,7 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
.updateCurrentGroupName(
|
||||
groupName,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
isSelected: groupName == state.currentGroupName,
|
||||
)
|
||||
@@ -126,18 +135,29 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
initialIndex: index == -1 ? 0 : index,
|
||||
vsync: this,
|
||||
);
|
||||
GroupNameKeyMap keyMap = {};
|
||||
final children = state.groupNames.map((groupName) {
|
||||
keyMap[groupName] = GlobalObjectKey(groupName);
|
||||
return KeepContainer(
|
||||
child: ProxyGroupView(
|
||||
key: keyMap[groupName],
|
||||
groupName: groupName,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
_keyMap = keyMap;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
NotificationListener<ScrollMetricsNotification>(
|
||||
onNotification: (scrollNotification) {
|
||||
hasMoreButtonNotifier.value =
|
||||
_hasMoreButtonNotifier.value =
|
||||
scrollNotification.metrics.maxScrollExtent > 0;
|
||||
return true;
|
||||
},
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: hasMoreButtonNotifier,
|
||||
valueListenable: _hasMoreButtonNotifier,
|
||||
builder: (_, value, child) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
@@ -199,16 +219,7 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
for (final groupName in state.groupNames)
|
||||
KeepContainer(
|
||||
key: ObjectKey(groupName),
|
||||
child: ProxyGroupView(
|
||||
groupName: groupName,
|
||||
type: ProxiesType.tab,
|
||||
),
|
||||
),
|
||||
],
|
||||
children: children,
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -217,3 +228,195 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProxyGroupView extends StatefulWidget {
|
||||
final String groupName;
|
||||
|
||||
const ProxyGroupView({
|
||||
super.key,
|
||||
required this.groupName,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProxyGroupView> createState() => ProxyGroupViewState();
|
||||
}
|
||||
|
||||
class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
var isLock = false;
|
||||
final _controller = ScrollController();
|
||||
List<Proxy> _lastProxies = [];
|
||||
|
||||
String get groupName => widget.groupName;
|
||||
|
||||
_delayTest(List<Proxy> proxies) async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
await delayTest(proxies);
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
scrollToSelected() {
|
||||
if (_controller.position.maxScrollExtent == 0) {
|
||||
return;
|
||||
}
|
||||
_controller.animateTo(
|
||||
16 +
|
||||
getScrollToSelectedOffset(
|
||||
groupName: groupName,
|
||||
proxies: _lastProxies,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, ProxyGroupSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final group = appState.getGroupWithName(groupName)!;
|
||||
return ProxyGroupSelectorState(
|
||||
proxyCardType: config.proxyCardType,
|
||||
proxiesSortType: config.proxiesSortType,
|
||||
columns: other.getProxiesColumns(
|
||||
appState.viewWidth,
|
||||
config.proxiesLayout,
|
||||
),
|
||||
sortNum: appState.sortNum,
|
||||
proxies: group.all,
|
||||
groupType: group.type,
|
||||
);
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
final proxies = state.proxies;
|
||||
final columns = state.columns;
|
||||
final proxyCardType = state.proxyCardType;
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
proxies,
|
||||
);
|
||||
_lastProxies = sortedProxies;
|
||||
return DelayTestButtonContainer(
|
||||
onClick: () async {
|
||||
await _delayTest(
|
||||
proxies,
|
||||
);
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GridView.builder(
|
||||
controller: _controller,
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: getItemHeight(proxyCardType),
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DelayTestButtonContainer extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Future Function() onClick;
|
||||
|
||||
const DelayTestButtonContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onClick,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DelayTestButtonContainer> createState() =>
|
||||
_DelayTestButtonContainerState();
|
||||
}
|
||||
|
||||
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scale;
|
||||
|
||||
_healthcheck() async {
|
||||
_controller.forward();
|
||||
await widget.onClick();
|
||||
_controller.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
);
|
||||
_scale = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(
|
||||
0,
|
||||
1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_controller.reverse();
|
||||
return FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
return SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: Transform.scale(
|
||||
scale: _scale.value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: _healthcheck,
|
||||
child: const Icon(Icons.network_ping),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,91 +22,11 @@ class GeoItem {
|
||||
});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class FileInfo {
|
||||
final String size;
|
||||
final DateTime lastModified;
|
||||
|
||||
const FileInfo({
|
||||
required this.size,
|
||||
required this.lastModified,
|
||||
});
|
||||
}
|
||||
|
||||
class Resources extends StatefulWidget {
|
||||
class Resources extends StatelessWidget {
|
||||
const Resources({super.key});
|
||||
|
||||
@override
|
||||
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() {
|
||||
Widget build(BuildContext context) {
|
||||
const geoItems = <GeoItem>[
|
||||
GeoItem(
|
||||
label: "GeoIp",
|
||||
@@ -122,26 +42,19 @@ class _ResourcesState extends State<Resources> {
|
||||
GeoItem(label: "ASN", fileName: asnFileName, key: "asn"),
|
||||
];
|
||||
|
||||
return generateInfoSection(
|
||||
info: Info(
|
||||
iconData: Icons.storage,
|
||||
label: appLocalizations.geoData,
|
||||
),
|
||||
items: geoItems.map(
|
||||
(geoItem) => GeoDataListItem(
|
||||
return ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
final geoItem = geoItems[index];
|
||||
return GeoDataListItem(
|
||||
geoItem: geoItem,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return generateListView(
|
||||
[
|
||||
..._buildGeoDataSection(),
|
||||
..._buildExternalProviderSection(),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: geoItems.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -178,7 +91,6 @@ 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,
|
||||
@@ -196,27 +108,11 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
final lastModified = await file.lastModified();
|
||||
final size = await file.length();
|
||||
return FileInfo(
|
||||
size: TrafficValue(value: size).show,
|
||||
size: size,
|
||||
lastModified: lastModified,
|
||||
);
|
||||
}
|
||||
|
||||
// _uploadGeoFile(String fileName) async {
|
||||
// final res = await picker.pickerGeoDataFile();
|
||||
// if (res == null || res.bytes == null) return;
|
||||
// final homePath = await appPath.getHomeDirPath();
|
||||
// final file = File(join(homePath, fileName));
|
||||
// await file.writeAsBytes(
|
||||
// res.bytes!,
|
||||
// flush: true,
|
||||
// );
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
String _buildFileInfoDesc(FileInfo fileInfo) {
|
||||
return "${fileInfo.size} · ${fileInfo.lastModified.lastUpdateTimeDesc}";
|
||||
}
|
||||
|
||||
Widget _buildSubtitle(String url) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -240,7 +136,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_buildFileInfoDesc(snapshot.data!),
|
||||
snapshot.data!.desc,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -253,9 +149,6 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 6,
|
||||
spacing: 12,
|
||||
@@ -288,9 +181,9 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
updateGeoDateItem() async {
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
final message = await clashCore.updateExternalProvider(
|
||||
providerName: geoItem.fileName,
|
||||
providerType: geoItem.label,
|
||||
final message = await clashCore.updateGeoData(
|
||||
geoName: geoItem.fileName,
|
||||
geoType: geoItem.label,
|
||||
);
|
||||
if (message.isNotEmpty) throw message;
|
||||
} catch (e) {
|
||||
@@ -342,117 +235,6 @@ 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;
|
||||
@@ -510,4 +292,4 @@ class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -23,35 +21,12 @@ class ThemeModeItem {
|
||||
class ThemeFragment extends StatelessWidget {
|
||||
const ThemeFragment({super.key});
|
||||
|
||||
Widget _itemCard({
|
||||
required BuildContext context,
|
||||
required Info info,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
),
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
InfoHeader(
|
||||
info: info,
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final previewCard = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CommonCard(
|
||||
onPressed: (){
|
||||
|
||||
},
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.preview,
|
||||
iconData: Icons.looks,
|
||||
@@ -110,7 +85,6 @@ class ThemeColorsBox extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
||||
|
||||
Widget _themeModeCheckBox({
|
||||
bool? isSelected,
|
||||
required ThemeModeItem themeModeItem,
|
||||
@@ -252,6 +226,27 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.prueBlack,
|
||||
builder: (_, value, ___) {
|
||||
return ListItem.switchItem(
|
||||
leading: Icon(
|
||||
Icons.contrast,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
title: Text(appLocalizations.prueBlackMode),
|
||||
delegate: SwitchDelegate(
|
||||
value: value,
|
||||
onChanged: (value){
|
||||
globalState.appController.config.prueBlack = value;
|
||||
}
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"overrideDesc": "Override Proxy related config",
|
||||
"allowLan": "AllowLan",
|
||||
"allowLanDesc": "Allow access proxy through the LAN",
|
||||
"tun": "TUN mode",
|
||||
"tun": "TUN",
|
||||
"tunDesc": "only effective in administrator mode",
|
||||
"minimizeOnExit": "Minimize on exit",
|
||||
"minimizeOnExitDesc": "Modify the default system exit event",
|
||||
@@ -66,6 +66,7 @@
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"minutes": "Minutes",
|
||||
"seconds": "Seconds",
|
||||
"ago": " Ago",
|
||||
"just": "Just",
|
||||
"qrcode": "QR code",
|
||||
@@ -116,7 +117,7 @@
|
||||
"logLevel": "LogLevel",
|
||||
"show": "Show",
|
||||
"exit": "Exit",
|
||||
"systemProxy": "SystemProxy",
|
||||
"systemProxy": "System proxy",
|
||||
"project": "Project",
|
||||
"core": "Core",
|
||||
"tabAnimation": "Tab animation",
|
||||
@@ -130,12 +131,10 @@
|
||||
"notSelectedTip": "The current proxy group cannot be selected.",
|
||||
"tip": "tip",
|
||||
"backupAndRecovery": "Backup and Recovery",
|
||||
"backupAndRecoveryDesc": "Sync data by WebDAV",
|
||||
"backupAndRecoveryDesc": "Sync data via WebDAV or file",
|
||||
"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",
|
||||
@@ -214,5 +213,35 @@
|
||||
"proxyGroup": "Proxy group",
|
||||
"go": "Go",
|
||||
"externalLink": "External link",
|
||||
"contributors": "Contributors"
|
||||
"otherContributors": "Other contributors",
|
||||
"autoCloseConnections": "Auto lose connections",
|
||||
"autoCloseConnectionsDesc": "Auto close connections after change node",
|
||||
"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"
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
"overrideDesc": "覆写代理相关配置",
|
||||
"allowLan": "局域网代理",
|
||||
"allowLanDesc": "允许通过局域网访问代理",
|
||||
"tun": "TUN模式",
|
||||
"tun": "虚拟网卡",
|
||||
"tunDesc": "仅在管理员模式生效",
|
||||
"minimizeOnExit": "退出时最小化",
|
||||
"minimizeOnExitDesc": "修改系统默认退出事件",
|
||||
@@ -66,6 +66,7 @@
|
||||
"hours": "小时",
|
||||
"days": "天",
|
||||
"minutes": "分钟",
|
||||
"seconds": "秒",
|
||||
"ago": "前",
|
||||
"just": "刚刚",
|
||||
"qrcode": "二维码",
|
||||
@@ -130,12 +131,10 @@
|
||||
"notSelectedTip": "当前代理组无法选中",
|
||||
"tip": "提示",
|
||||
"backupAndRecovery": "备份与恢复",
|
||||
"backupAndRecoveryDesc": "通过WebDAV同步数据",
|
||||
"backupAndRecoveryDesc": "通过WebDAV或者文件同步数据",
|
||||
"account": "账号",
|
||||
"backup": "备份",
|
||||
"backupDesc": "备份数据到WebDAV",
|
||||
"recovery": "恢复",
|
||||
"recoveryDesc": "从WebDAV恢复数据",
|
||||
"recoveryProfiles": "仅恢复配置文件",
|
||||
"recoveryAll": "恢复所有数据",
|
||||
"recoverySuccess": "恢复成功",
|
||||
@@ -214,5 +213,35 @@
|
||||
"proxyGroup": "代理组",
|
||||
"go": "前往",
|
||||
"externalLink": "外部链接",
|
||||
"contributors": "贡献者"
|
||||
"otherContributors": "其他贡献者",
|
||||
"autoCloseConnections": "自动关闭连接",
|
||||
"autoCloseConnectionsDesc": "切换节点后自动关闭连接",
|
||||
"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": "暂停"
|
||||
}
|
||||
@@ -33,6 +33,7 @@ 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":
|
||||
@@ -40,6 +41,7 @@ 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(
|
||||
@@ -58,6 +60,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Auto check updates"),
|
||||
"autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Auto check for updates when the app starts"),
|
||||
"autoCloseConnections":
|
||||
MessageLookupByLibrary.simpleMessage("Auto lose connections"),
|
||||
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Auto close connections after change node"),
|
||||
"autoLaunch": MessageLookupByLibrary.simpleMessage("AutoLaunch"),
|
||||
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Follow the system self startup"),
|
||||
@@ -70,10 +76,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"backup": MessageLookupByLibrary.simpleMessage("Backup"),
|
||||
"backupAndRecovery":
|
||||
MessageLookupByLibrary.simpleMessage("Backup and Recovery"),
|
||||
"backupAndRecoveryDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Sync data by WebDAV"),
|
||||
"backupDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"),
|
||||
"backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Sync data via WebDAV or file"),
|
||||
"backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"),
|
||||
"bind": MessageLookupByLibrary.simpleMessage("Bind"),
|
||||
"blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"),
|
||||
@@ -87,6 +91,10 @@ 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"),
|
||||
@@ -97,7 +105,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"connectionsDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"View current connections data"),
|
||||
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity:"),
|
||||
"contributors": MessageLookupByLibrary.simpleMessage("Contributors"),
|
||||
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
|
||||
"core": MessageLookupByLibrary.simpleMessage("Core"),
|
||||
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
|
||||
@@ -112,6 +119,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"delay": MessageLookupByLibrary.simpleMessage("Delay"),
|
||||
"delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
|
||||
"deleteProfileTip": MessageLookupByLibrary.simpleMessage(
|
||||
"Sure you want to delete the current profile?"),
|
||||
"desc": MessageLookupByLibrary.simpleMessage(
|
||||
"A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free."),
|
||||
"direct": MessageLookupByLibrary.simpleMessage("Direct"),
|
||||
@@ -124,6 +133,7 @@ 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(
|
||||
@@ -163,25 +173,37 @@ 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"),
|
||||
@@ -205,7 +227,15 @@ 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(
|
||||
"When turned on, only statistics proxy traffic"),
|
||||
"other": MessageLookupByLibrary.simpleMessage("Other"),
|
||||
"otherContributors":
|
||||
MessageLookupByLibrary.simpleMessage("Other contributors"),
|
||||
"outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"),
|
||||
"override": MessageLookupByLibrary.simpleMessage("Override"),
|
||||
"overrideDesc": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -238,6 +268,7 @@ 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":
|
||||
@@ -246,18 +277,23 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
|
||||
"proxyPortDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Set the Clash listening port"),
|
||||
"prueBlackMode":
|
||||
MessageLookupByLibrary.simpleMessage("Prue black mode"),
|
||||
"qrcode": MessageLookupByLibrary.simpleMessage("QR code"),
|
||||
"qrcodeDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Scan QR code to obtain profile"),
|
||||
"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"),
|
||||
@@ -267,6 +303,7 @@ 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"),
|
||||
@@ -277,12 +314,16 @@ 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("SystemProxy"),
|
||||
"systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"),
|
||||
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Attach HTTP proxy to VpnService"),
|
||||
"tab": MessageLookupByLibrary.simpleMessage("Tab"),
|
||||
@@ -299,10 +340,12 @@ 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 mode"),
|
||||
"tun": MessageLookupByLibrary.simpleMessage("TUN"),
|
||||
"tunDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"only effective in administrator mode"),
|
||||
"twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"),
|
||||
|
||||
@@ -31,11 +31,13 @@ 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"),
|
||||
@@ -49,6 +51,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"),
|
||||
"autoCheckUpdateDesc":
|
||||
MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"),
|
||||
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"),
|
||||
"autoCloseConnectionsDesc":
|
||||
MessageLookupByLibrary.simpleMessage("切换节点后自动关闭连接"),
|
||||
"autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"),
|
||||
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"),
|
||||
"autoRun": MessageLookupByLibrary.simpleMessage("自动运行"),
|
||||
@@ -59,8 +64,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"backup": MessageLookupByLibrary.simpleMessage("备份"),
|
||||
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
|
||||
"backupAndRecoveryDesc":
|
||||
MessageLookupByLibrary.simpleMessage("通过WebDAV同步数据"),
|
||||
"backupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
|
||||
MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"),
|
||||
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
|
||||
"bind": MessageLookupByLibrary.simpleMessage("绑定"),
|
||||
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
|
||||
@@ -71,6 +75,8 @@ 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":
|
||||
@@ -79,7 +85,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"connections": MessageLookupByLibrary.simpleMessage("连接"),
|
||||
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
|
||||
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
|
||||
"contributors": MessageLookupByLibrary.simpleMessage("贡献者"),
|
||||
"copy": MessageLookupByLibrary.simpleMessage("复制"),
|
||||
"core": MessageLookupByLibrary.simpleMessage("内核"),
|
||||
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
|
||||
@@ -94,6 +99,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
|
||||
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
||||
"deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"),
|
||||
"desc": MessageLookupByLibrary.simpleMessage(
|
||||
"基于ClashMeta的多平台代理客户端,简单易用,开源无广告。"),
|
||||
"direct": MessageLookupByLibrary.simpleMessage("直连"),
|
||||
@@ -103,6 +109,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"download": MessageLookupByLibrary.simpleMessage("下载"),
|
||||
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
||||
"en": MessageLookupByLibrary.simpleMessage("英语"),
|
||||
"entries": MessageLookupByLibrary.simpleMessage("个条目"),
|
||||
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
|
||||
"excludeDesc":
|
||||
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
|
||||
@@ -133,22 +140,31 @@ 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("名称"),
|
||||
@@ -168,7 +184,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
|
||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
|
||||
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
|
||||
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
|
||||
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
|
||||
"onlyStatisticsProxyDesc":
|
||||
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"),
|
||||
"other": MessageLookupByLibrary.simpleMessage("其他"),
|
||||
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
|
||||
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
|
||||
"override": MessageLookupByLibrary.simpleMessage("覆写"),
|
||||
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
|
||||
@@ -195,19 +216,24 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"profileUrlNullValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请输入配置URL"),
|
||||
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
|
||||
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
|
||||
"project": MessageLookupByLibrary.simpleMessage("项目"),
|
||||
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
|
||||
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
|
||||
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
|
||||
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
|
||||
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
|
||||
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
|
||||
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
|
||||
"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("资源"),
|
||||
@@ -215,6 +241,7 @@ 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("设置"),
|
||||
@@ -224,7 +251,11 @@ 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("提交"),
|
||||
@@ -244,10 +275,12 @@ 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模式"),
|
||||
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
|
||||
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
|
||||
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
|
||||
"unableToUpdateCurrentProfileDesc":
|
||||
|
||||
@@ -430,10 +430,10 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `TUN mode`
|
||||
/// `TUN`
|
||||
String get tun {
|
||||
return Intl.message(
|
||||
'TUN mode',
|
||||
'TUN',
|
||||
name: 'tun',
|
||||
desc: '',
|
||||
args: [],
|
||||
@@ -720,6 +720,16 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Seconds`
|
||||
String get seconds {
|
||||
return Intl.message(
|
||||
'Seconds',
|
||||
name: 'seconds',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// ` Ago`
|
||||
String get ago {
|
||||
return Intl.message(
|
||||
@@ -1220,10 +1230,10 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `SystemProxy`
|
||||
/// `System proxy`
|
||||
String get systemProxy {
|
||||
return Intl.message(
|
||||
'SystemProxy',
|
||||
'System proxy',
|
||||
name: 'systemProxy',
|
||||
desc: '',
|
||||
args: [],
|
||||
@@ -1360,10 +1370,10 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Sync data by WebDAV`
|
||||
/// `Sync data via WebDAV or file`
|
||||
String get backupAndRecoveryDesc {
|
||||
return Intl.message(
|
||||
'Sync data by WebDAV',
|
||||
'Sync data via WebDAV or file',
|
||||
name: 'backupAndRecoveryDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
@@ -1390,16 +1400,6 @@ 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,16 +1410,6 @@ 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(
|
||||
@@ -2200,11 +2190,311 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Contributors`
|
||||
String get contributors {
|
||||
/// `Other contributors`
|
||||
String get otherContributors {
|
||||
return Intl.message(
|
||||
'Contributors',
|
||||
name: 'contributors',
|
||||
'Other contributors',
|
||||
name: 'otherContributors',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Auto lose connections`
|
||||
String get autoCloseConnections {
|
||||
return Intl.message(
|
||||
'Auto lose connections',
|
||||
name: 'autoCloseConnections',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Auto close connections after change node`
|
||||
String get autoCloseConnectionsDesc {
|
||||
return Intl.message(
|
||||
'Auto close connections after change node',
|
||||
name: 'autoCloseConnectionsDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Only statistics proxy`
|
||||
String get onlyStatisticsProxy {
|
||||
return Intl.message(
|
||||
'Only statistics proxy',
|
||||
name: 'onlyStatisticsProxy',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `When turned on, only statistics proxy traffic`
|
||||
String get onlyStatisticsProxyDesc {
|
||||
return Intl.message(
|
||||
'When turned on, only statistics proxy traffic',
|
||||
name: 'onlyStatisticsProxyDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Sure you want to delete the current profile?`
|
||||
String get deleteProfileTip {
|
||||
return Intl.message(
|
||||
'Sure you want to delete the current profile?',
|
||||
name: 'deleteProfileTip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Prue black mode`
|
||||
String get prueBlackMode {
|
||||
return Intl.message(
|
||||
'Prue black mode',
|
||||
name: 'prueBlackMode',
|
||||
desc: '',
|
||||
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: [],
|
||||
);
|
||||
|
||||
@@ -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,6 +18,7 @@ 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);
|
||||
@@ -47,6 +48,7 @@ Future<void> main() async {
|
||||
Future<void> vpnService() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
globalState.isVpnService = true;
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
|
||||
final appState = AppState(
|
||||
@@ -60,14 +62,14 @@ Future<void> vpnService() async {
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
|
||||
proxy?.setServiceMessageHandler(
|
||||
vpn?.setServiceMessageHandler(
|
||||
ServiceMessageHandler(
|
||||
onProtect: (Fd fd) async {
|
||||
await proxy?.setProtect(fd.value);
|
||||
await vpn?.setProtect(fd.value);
|
||||
clashCore.setFdMap(fd.id);
|
||||
},
|
||||
onProcess: (Process process) async {
|
||||
var packageName = await app?.resolverProcess(process);
|
||||
final packageName = await app?.resolverProcess(process);
|
||||
clashCore.setProcessMap(
|
||||
ProcessMapItem(
|
||||
id: process.id,
|
||||
@@ -75,8 +77,8 @@ Future<void> vpnService() async {
|
||||
),
|
||||
);
|
||||
},
|
||||
onStarted: (String runTime) {
|
||||
globalState.applyProfile(
|
||||
onStarted: (String runTime) async {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
@@ -86,11 +88,10 @@ Future<void> vpnService() async {
|
||||
final currentSelectedMap = config.currentSelectedMap;
|
||||
final proxyName = currentSelectedMap[groupName];
|
||||
if (proxyName == null) return;
|
||||
clashCore.changeProxy(
|
||||
ChangeProxyParams(
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
),
|
||||
globalState.changeProxy(
|
||||
config: config,
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -100,8 +101,7 @@ Future<void> vpnService() async {
|
||||
WidgetsBinding.instance.platformDispatcher.locale,
|
||||
);
|
||||
await app?.tip(appLocalizations.startVpn);
|
||||
await globalState.startSystemProxy(
|
||||
appState: appState,
|
||||
await globalState.handleStart(
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
@@ -110,7 +110,7 @@ Future<void> vpnService() async {
|
||||
TileListenerWithVpn(
|
||||
onStop: () async {
|
||||
await app?.tip(appLocalizations.stopVpn);
|
||||
await globalState.stopSystemProxy();
|
||||
await globalState.handleStop();
|
||||
clashCore.shutdown();
|
||||
exit(0);
|
||||
},
|
||||
@@ -119,7 +119,7 @@ Future<void> vpnService() async {
|
||||
|
||||
globalState.updateTraffic();
|
||||
globalState.updateFunctionLists = [
|
||||
() {
|
||||
() {
|
||||
globalState.updateTraffic();
|
||||
}
|
||||
];
|
||||
@@ -130,15 +130,14 @@ class ServiceMessageHandler with ServiceMessageListener {
|
||||
final Function(Fd fd) _onProtect;
|
||||
final Function(Process process) _onProcess;
|
||||
final Function(String runTime) _onStarted;
|
||||
final Function(String groupName) _onLoaded;
|
||||
final Function(String providerName) _onLoaded;
|
||||
|
||||
const ServiceMessageHandler({
|
||||
required Function(Fd fd) onProtect,
|
||||
required Function(Process process) onProcess,
|
||||
required Function(String runTime) onStarted,
|
||||
required Function(String groupName) onLoaded,
|
||||
})
|
||||
: _onProtect = onProtect,
|
||||
required Function(String providerName) onLoaded,
|
||||
}) : _onProtect = onProtect,
|
||||
_onProcess = onProcess,
|
||||
_onStarted = onStarted,
|
||||
_onLoaded = onLoaded;
|
||||
@@ -159,8 +158,8 @@ class ServiceMessageHandler with ServiceMessageListener {
|
||||
}
|
||||
|
||||
@override
|
||||
onLoaded(String groupName) {
|
||||
_onLoaded(groupName);
|
||||
onLoaded(String providerName) {
|
||||
_onLoaded(providerName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -35,6 +36,8 @@ class AppState with ChangeNotifier {
|
||||
double _viewWidth;
|
||||
List<Connection> _requests;
|
||||
num _checkIpNum;
|
||||
List<ExternalProvider> _providers;
|
||||
List<Package> _packages;
|
||||
|
||||
AppState({
|
||||
required Mode mode,
|
||||
@@ -54,8 +57,10 @@ class AppState with ChangeNotifier {
|
||||
_totalTraffic = Traffic(),
|
||||
_delayMap = {},
|
||||
_groups = [],
|
||||
_providers = [],
|
||||
_packages = [],
|
||||
_isCompatible = isCompatible,
|
||||
_systemColorSchemes = SystemColorSchemes();
|
||||
_systemColorSchemes = const SystemColorSchemes();
|
||||
|
||||
String get currentLabel => _currentLabel;
|
||||
|
||||
@@ -109,7 +114,7 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String getDesc(String type, String? proxyName) {
|
||||
String getDesc(String type, String proxyName) {
|
||||
final groupTypeNamesList = GroupType.values.map((e) => e.name).toList();
|
||||
if (!groupTypeNamesList.contains(type)) {
|
||||
return type;
|
||||
@@ -120,15 +125,17 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String? getRealProxyName(String? proxyName) {
|
||||
if (proxyName == null) return null;
|
||||
String getRealProxyName(String proxyName) {
|
||||
if (proxyName.isEmpty) return proxyName;
|
||||
final index = groups.indexWhere((element) => element.name == proxyName);
|
||||
if (index == -1) return proxyName;
|
||||
final group = groups[index];
|
||||
return getRealProxyName((selectedMap.containsKey(proxyName)
|
||||
? selectedMap[proxyName]
|
||||
: group.now)) ??
|
||||
proxyName;
|
||||
final currentSelectedName =
|
||||
group.getCurrentSelectedName(selectedMap[proxyName] ?? '');
|
||||
if (currentSelectedName.isEmpty) return proxyName;
|
||||
return getRealProxyName(
|
||||
currentSelectedName,
|
||||
);
|
||||
}
|
||||
|
||||
String? get showProxyName {
|
||||
@@ -140,7 +147,7 @@ class AppState with ChangeNotifier {
|
||||
return selectedMap[firstGroupName] ?? firstGroup.now;
|
||||
}
|
||||
|
||||
int? getDelay(String? proxyName) {
|
||||
int? getDelay(String proxyName) {
|
||||
return _delayMap[getRealProxyName(proxyName)];
|
||||
}
|
||||
|
||||
@@ -293,6 +300,7 @@ class AppState with ChangeNotifier {
|
||||
.toList();
|
||||
case Mode.rule:
|
||||
return groups
|
||||
.where((item) => item.hidden == false)
|
||||
.where((element) => element.name != GroupName.GLOBAL.name)
|
||||
.toList();
|
||||
}
|
||||
@@ -327,6 +335,32 @@ 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);
|
||||
|
||||
@@ -119,6 +119,7 @@ class ClashConfig extends ChangeNotifier {
|
||||
String _externalController;
|
||||
Mode _mode;
|
||||
FindProcessMode _findProcessMode;
|
||||
int _keepAliveInterval;
|
||||
bool _unifiedDelay;
|
||||
bool _tcpConcurrent;
|
||||
Tun _tun;
|
||||
@@ -139,6 +140,7 @@ class ClashConfig extends ChangeNotifier {
|
||||
_unifiedDelay = false,
|
||||
_geodataLoader = geodataLoaderMemconservative,
|
||||
_externalController = '',
|
||||
_keepAliveInterval = 30,
|
||||
_dns = Dns(),
|
||||
_geoXUrl = defaultGeoXMap,
|
||||
_rules = [];
|
||||
@@ -203,6 +205,16 @@ 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;
|
||||
|
||||
@@ -275,7 +287,7 @@ class ClashConfig extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(name: "global-ua", defaultValue: null)
|
||||
@JsonKey(name: "global-ua", includeFromJson: false, includeToJson: true)
|
||||
String get globalUa {
|
||||
if (_globalRealUa == null) {
|
||||
return globalState.packageInfo.ua;
|
||||
@@ -320,7 +332,6 @@ class ClashConfig extends ChangeNotifier {
|
||||
_geodataLoader = clashConfig._geodataLoader;
|
||||
_dns = clashConfig._dns;
|
||||
_rules = clashConfig._rules;
|
||||
_globalRealUa = clashConfig.globalRealUa;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
|
||||
@@ -25,15 +26,27 @@ class AccessControl with _$AccessControl {
|
||||
_$AccessControlFromJson(json);
|
||||
}
|
||||
|
||||
extension AccessControlExt on AccessControl {
|
||||
List<String> get currentList => switch (mode) {
|
||||
AccessControlMode.acceptSelected => acceptList,
|
||||
AccessControlMode.rejectSelected => rejectList,
|
||||
};
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Props with _$Props {
|
||||
const factory Props({
|
||||
class CoreState with _$CoreState {
|
||||
const factory CoreState({
|
||||
AccessControl? accessControl,
|
||||
required String currentProfileName,
|
||||
required bool enable,
|
||||
required bool allowBypass,
|
||||
required bool systemProxy,
|
||||
}) = _Props;
|
||||
required int mixedPort,
|
||||
required bool onlyProxy,
|
||||
}) = _CoreState;
|
||||
|
||||
factory Props.fromJson(Map<String, Object?> json) => _$PropsFromJson(json);
|
||||
factory CoreState.fromJson(Map<String, Object?> json) =>
|
||||
_$CoreStateFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -46,10 +59,30 @@ class WindowProps with _$WindowProps {
|
||||
}) = _WindowProps;
|
||||
|
||||
factory WindowProps.fromJson(Map<String, Object?>? json) =>
|
||||
json == null ? defaultWindowProps : _$WindowPropsFromJson(json);
|
||||
json == null ? const WindowProps() : _$WindowPropsFromJson(json);
|
||||
}
|
||||
|
||||
const defaultWindowProps = WindowProps();
|
||||
@freezed
|
||||
class VpnProps with _$VpnProps {
|
||||
const factory VpnProps({
|
||||
@Default(true) bool enable,
|
||||
@Default(false) bool systemProxy,
|
||||
@Default(true) bool allowBypass,
|
||||
}) = _VpnProps;
|
||||
|
||||
factory VpnProps.fromJson(Map<String, Object?>? json) =>
|
||||
json == null ? const VpnProps() : _$VpnPropsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DesktopProps with _$DesktopProps {
|
||||
const factory DesktopProps({
|
||||
@Default(true) bool systemProxy,
|
||||
}) = _DesktopProps;
|
||||
|
||||
factory DesktopProps.fromJson(Map<String, Object?>? json) =>
|
||||
json == null ? const DesktopProps() : _$DesktopPropsFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Config extends ChangeNotifier {
|
||||
@@ -69,21 +102,26 @@ class Config extends ChangeNotifier {
|
||||
AccessControl _accessControl;
|
||||
bool _isAnimateToPage;
|
||||
bool _autoCheckUpdate;
|
||||
bool _allowBypass;
|
||||
bool _systemProxy;
|
||||
bool _isExclude;
|
||||
DAV? _dav;
|
||||
bool _isCloseConnections;
|
||||
ProxiesType _proxiesType;
|
||||
ProxyCardType _proxyCardType;
|
||||
int _proxiesColumns;
|
||||
ProxiesLayout _proxiesLayout;
|
||||
String _testUrl;
|
||||
WindowProps _windowProps;
|
||||
bool _onlyProxy;
|
||||
bool _prueBlack;
|
||||
VpnProps _vpnProps;
|
||||
DesktopProps _desktopProps;
|
||||
bool _showLabel;
|
||||
|
||||
Config()
|
||||
: _profiles = [],
|
||||
_autoLaunch = false,
|
||||
_silentLaunch = false,
|
||||
_autoRun = false,
|
||||
_isCloseConnections = false,
|
||||
_themeMode = ThemeMode.system,
|
||||
_openLog = false,
|
||||
_isCompatible = true,
|
||||
@@ -92,16 +130,19 @@ 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 = defaultWindowProps,
|
||||
_windowProps = const WindowProps(),
|
||||
_proxiesType = ProxiesType.tab,
|
||||
_proxiesColumns = 2;
|
||||
_prueBlack = false,
|
||||
_onlyProxy = false,
|
||||
_proxiesLayout = ProxiesLayout.standard,
|
||||
_vpnProps = const VpnProps(),
|
||||
_desktopProps = const DesktopProps(),
|
||||
_showLabel = false;
|
||||
|
||||
deleteProfileById(String id) {
|
||||
_profiles = profiles.where((element) => element.id != id).toList();
|
||||
@@ -303,6 +344,16 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: ProxiesLayout.standard)
|
||||
ProxiesLayout get proxiesLayout => _proxiesLayout;
|
||||
|
||||
set proxiesLayout(ProxiesLayout value) {
|
||||
if (_proxiesLayout != value) {
|
||||
_proxiesLayout = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool get isMinimizeOnExit => _isMinimizeOnExit;
|
||||
|
||||
@@ -381,26 +432,38 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool get allowBypass {
|
||||
return _allowBypass;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool get onlyProxy {
|
||||
return _onlyProxy;
|
||||
}
|
||||
|
||||
set allowBypass(bool value) {
|
||||
if (_allowBypass != value) {
|
||||
_allowBypass = value;
|
||||
set onlyProxy(bool value) {
|
||||
if (_onlyProxy != value) {
|
||||
_onlyProxy = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool get systemProxy {
|
||||
return _systemProxy;
|
||||
bool get prueBlack {
|
||||
return _prueBlack;
|
||||
}
|
||||
|
||||
set systemProxy(bool value) {
|
||||
if (_systemProxy != value) {
|
||||
_systemProxy = value;
|
||||
set prueBlack(bool value) {
|
||||
if (_prueBlack != value) {
|
||||
_prueBlack = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool get isCloseConnections {
|
||||
return _isCloseConnections;
|
||||
}
|
||||
|
||||
set isCloseConnections(bool value) {
|
||||
if (_isCloseConnections != value) {
|
||||
_isCloseConnections = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -428,16 +491,6 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: 2)
|
||||
int get proxiesColumns => _proxiesColumns;
|
||||
|
||||
set proxiesColumns(int value) {
|
||||
if (_proxiesColumns != value) {
|
||||
_proxiesColumns = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(name: "test-url", defaultValue: defaultTestUrl)
|
||||
String get testUrl => _testUrl;
|
||||
|
||||
@@ -467,6 +520,34 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
VpnProps get vpnProps => _vpnProps;
|
||||
|
||||
set vpnProps(VpnProps value) {
|
||||
if (_vpnProps != value) {
|
||||
_vpnProps = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
DesktopProps get desktopProps => _desktopProps;
|
||||
|
||||
set desktopProps(DesktopProps value) {
|
||||
if (_desktopProps != value) {
|
||||
_desktopProps = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool get showLabel => _showLabel;
|
||||
|
||||
set showLabel(bool value) {
|
||||
if (_showLabel != value) {
|
||||
_showLabel = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
update([
|
||||
Config? config,
|
||||
RecoveryOption recoveryOptions = RecoveryOption.all,
|
||||
@@ -482,6 +563,7 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
if (onlyProfiles) return;
|
||||
_currentProfileId = config._currentProfileId;
|
||||
_isCloseConnections = config._isCloseConnections;
|
||||
_isCompatible = config._isCompatible;
|
||||
_autoLaunch = config._autoLaunch;
|
||||
_silentLaunch = config._silentLaunch;
|
||||
@@ -490,7 +572,6 @@ 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;
|
||||
@@ -498,9 +579,12 @@ class Config extends ChangeNotifier {
|
||||
_accessControl = config._accessControl;
|
||||
_isAnimateToPage = config._isAnimateToPage;
|
||||
_autoCheckUpdate = config._autoCheckUpdate;
|
||||
_prueBlack = config._prueBlack;
|
||||
_testUrl = config._testUrl;
|
||||
_isExclude = config._isExclude;
|
||||
_windowProps = config._windowProps;
|
||||
_vpnProps = config._vpnProps;
|
||||
_desktopProps = config._desktopProps;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// ignore_for_file: invalid_annotation_target
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/clash_config.dart';
|
||||
import 'package:fl_clash/models/connection.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@@ -26,7 +24,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams {
|
||||
@freezed
|
||||
class UpdateConfigParams with _$UpdateConfigParams {
|
||||
const factory UpdateConfigParams({
|
||||
@JsonKey(name: "profile-path") String? profilePath,
|
||||
@JsonKey(name: "profile-id") required String profileId,
|
||||
required ClashConfig config,
|
||||
required ConfigExtendedParams params,
|
||||
}) = _UpdateConfigParams;
|
||||
@@ -125,6 +123,9 @@ 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;
|
||||
@@ -142,7 +143,7 @@ abstract mixin class AppMessageListener {
|
||||
|
||||
void onStarted(String runTime) {}
|
||||
|
||||
void onLoaded(String groupName) {}
|
||||
void onLoaded(String providerName) {}
|
||||
}
|
||||
|
||||
abstract mixin class ServiceMessageListener {
|
||||
@@ -152,7 +153,5 @@ abstract mixin class ServiceMessageListener {
|
||||
|
||||
onStarted(String runTime) {}
|
||||
|
||||
onLoaded(String groupName) {}
|
||||
onLoaded(String providerName) {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
21
lib/models/file.dart
Normal file
21
lib/models/file.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:fl_clash/common/datetime.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'traffic.dart';
|
||||
|
||||
part 'generated/file.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class FileInfo with _$FileInfo {
|
||||
const factory FileInfo({
|
||||
required int size,
|
||||
required DateTime lastModified,
|
||||
}) = _FileInfo;
|
||||
}
|
||||
|
||||
|
||||
extension FileInfoExt on FileInfo{
|
||||
String get desc => "${TrafficValue(value: size).show} · ${lastModified.lastUpdateTimeDesc}";
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ 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
|
||||
@@ -75,6 +76,7 @@ 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,
|
||||
@@ -82,6 +84,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
|
||||
'tun': instance.tun,
|
||||
'dns': instance.dns,
|
||||
'rules': instance.rules,
|
||||
'global-ua': instance.globalUa,
|
||||
'global-real-ua': instance.globalRealUa,
|
||||
'geox-url': instance.geoXUrl,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -41,6 +42,7 @@ abstract class $AccessControlCopyWith<$Res> {
|
||||
{AccessControlMode mode,
|
||||
List<String> acceptList,
|
||||
List<String> rejectList,
|
||||
AccessSortType sort,
|
||||
bool isFilterSystemApp});
|
||||
}
|
||||
|
||||
@@ -60,6 +62,7 @@ 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(
|
||||
@@ -75,6 +78,10 @@ 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
|
||||
@@ -95,6 +102,7 @@ abstract class _$$AccessControlImplCopyWith<$Res>
|
||||
{AccessControlMode mode,
|
||||
List<String> acceptList,
|
||||
List<String> rejectList,
|
||||
AccessSortType sort,
|
||||
bool isFilterSystemApp});
|
||||
}
|
||||
|
||||
@@ -112,6 +120,7 @@ class __$$AccessControlImplCopyWithImpl<$Res>
|
||||
Object? mode = null,
|
||||
Object? acceptList = null,
|
||||
Object? rejectList = null,
|
||||
Object? sort = null,
|
||||
Object? isFilterSystemApp = null,
|
||||
}) {
|
||||
return _then(_$AccessControlImpl(
|
||||
@@ -127,6 +136,10 @@ 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
|
||||
@@ -142,6 +155,7 @@ 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;
|
||||
@@ -170,13 +184,16 @@ 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, isFilterSystemApp: $isFilterSystemApp)';
|
||||
return 'AccessControl(mode: $mode, acceptList: $acceptList, rejectList: $rejectList, sort: $sort, isFilterSystemApp: $isFilterSystemApp)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -189,6 +206,7 @@ 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));
|
||||
}
|
||||
@@ -200,6 +218,7 @@ class _$AccessControlImpl implements _AccessControl {
|
||||
mode,
|
||||
const DeepCollectionEquality().hash(_acceptList),
|
||||
const DeepCollectionEquality().hash(_rejectList),
|
||||
sort,
|
||||
isFilterSystemApp);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@@ -221,6 +240,7 @@ 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) =
|
||||
@@ -233,6 +253,8 @@ abstract class _AccessControl implements AccessControl {
|
||||
@override
|
||||
List<String> get rejectList;
|
||||
@override
|
||||
AccessSortType get sort;
|
||||
@override
|
||||
bool get isFilterSystemApp;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
@@ -240,35 +262,47 @@ abstract class _AccessControl implements AccessControl {
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
Props _$PropsFromJson(Map<String, dynamic> json) {
|
||||
return _Props.fromJson(json);
|
||||
CoreState _$CoreStateFromJson(Map<String, dynamic> json) {
|
||||
return _CoreState.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Props {
|
||||
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;
|
||||
bool get onlyProxy => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$PropsCopyWith<Props> get copyWith => throw _privateConstructorUsedError;
|
||||
$CoreStateCopyWith<CoreState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $PropsCopyWith<$Res> {
|
||||
factory $PropsCopyWith(Props value, $Res Function(Props) then) =
|
||||
_$PropsCopyWithImpl<$Res, Props>;
|
||||
abstract class $CoreStateCopyWith<$Res> {
|
||||
factory $CoreStateCopyWith(CoreState value, $Res Function(CoreState) then) =
|
||||
_$CoreStateCopyWithImpl<$Res, CoreState>;
|
||||
@useResult
|
||||
$Res call({AccessControl? accessControl, bool allowBypass, bool systemProxy});
|
||||
$Res call(
|
||||
{AccessControl? accessControl,
|
||||
String currentProfileName,
|
||||
bool enable,
|
||||
bool allowBypass,
|
||||
bool systemProxy,
|
||||
int mixedPort,
|
||||
bool onlyProxy});
|
||||
|
||||
$AccessControlCopyWith<$Res>? get accessControl;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$PropsCopyWithImpl<$Res, $Val extends Props>
|
||||
implements $PropsCopyWith<$Res> {
|
||||
_$PropsCopyWithImpl(this._value, this._then);
|
||||
class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
|
||||
implements $CoreStateCopyWith<$Res> {
|
||||
_$CoreStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
@@ -279,14 +313,26 @@ class _$PropsCopyWithImpl<$Res, $Val extends Props>
|
||||
@override
|
||||
$Res call({
|
||||
Object? accessControl = freezed,
|
||||
Object? currentProfileName = null,
|
||||
Object? enable = null,
|
||||
Object? allowBypass = null,
|
||||
Object? systemProxy = null,
|
||||
Object? mixedPort = null,
|
||||
Object? onlyProxy = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
accessControl: freezed == accessControl
|
||||
? _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
|
||||
@@ -295,6 +341,14 @@ class _$PropsCopyWithImpl<$Res, $Val extends Props>
|
||||
? _value.systemProxy
|
||||
: systemProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
mixedPort: null == mixedPort
|
||||
? _value.mixedPort
|
||||
: mixedPort // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@@ -312,38 +366,58 @@ class _$PropsCopyWithImpl<$Res, $Val extends Props>
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$PropsImplCopyWith<$Res> implements $PropsCopyWith<$Res> {
|
||||
factory _$$PropsImplCopyWith(
|
||||
_$PropsImpl value, $Res Function(_$PropsImpl) then) =
|
||||
__$$PropsImplCopyWithImpl<$Res>;
|
||||
abstract class _$$CoreStateImplCopyWith<$Res>
|
||||
implements $CoreStateCopyWith<$Res> {
|
||||
factory _$$CoreStateImplCopyWith(
|
||||
_$CoreStateImpl value, $Res Function(_$CoreStateImpl) then) =
|
||||
__$$CoreStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({AccessControl? accessControl, bool allowBypass, bool systemProxy});
|
||||
$Res call(
|
||||
{AccessControl? accessControl,
|
||||
String currentProfileName,
|
||||
bool enable,
|
||||
bool allowBypass,
|
||||
bool systemProxy,
|
||||
int mixedPort,
|
||||
bool onlyProxy});
|
||||
|
||||
@override
|
||||
$AccessControlCopyWith<$Res>? get accessControl;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$PropsImplCopyWithImpl<$Res>
|
||||
extends _$PropsCopyWithImpl<$Res, _$PropsImpl>
|
||||
implements _$$PropsImplCopyWith<$Res> {
|
||||
__$$PropsImplCopyWithImpl(
|
||||
_$PropsImpl _value, $Res Function(_$PropsImpl) _then)
|
||||
class __$$CoreStateImplCopyWithImpl<$Res>
|
||||
extends _$CoreStateCopyWithImpl<$Res, _$CoreStateImpl>
|
||||
implements _$$CoreStateImplCopyWith<$Res> {
|
||||
__$$CoreStateImplCopyWithImpl(
|
||||
_$CoreStateImpl _value, $Res Function(_$CoreStateImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? accessControl = freezed,
|
||||
Object? currentProfileName = null,
|
||||
Object? enable = null,
|
||||
Object? allowBypass = null,
|
||||
Object? systemProxy = null,
|
||||
Object? mixedPort = null,
|
||||
Object? onlyProxy = null,
|
||||
}) {
|
||||
return _then(_$PropsImpl(
|
||||
return _then(_$CoreStateImpl(
|
||||
accessControl: freezed == accessControl
|
||||
? _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
|
||||
@@ -352,82 +426,129 @@ class __$$PropsImplCopyWithImpl<$Res>
|
||||
? _value.systemProxy
|
||||
: systemProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
mixedPort: null == mixedPort
|
||||
? _value.mixedPort
|
||||
: mixedPort // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$PropsImpl implements _Props {
|
||||
const _$PropsImpl(
|
||||
class _$CoreStateImpl implements _CoreState {
|
||||
const _$CoreStateImpl(
|
||||
{this.accessControl,
|
||||
required this.currentProfileName,
|
||||
required this.enable,
|
||||
required this.allowBypass,
|
||||
required this.systemProxy});
|
||||
required this.systemProxy,
|
||||
required this.mixedPort,
|
||||
required this.onlyProxy});
|
||||
|
||||
factory _$PropsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$PropsImplFromJson(json);
|
||||
factory _$CoreStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$CoreStateImplFromJson(json);
|
||||
|
||||
@override
|
||||
final AccessControl? accessControl;
|
||||
@override
|
||||
final String currentProfileName;
|
||||
@override
|
||||
final bool enable;
|
||||
@override
|
||||
final bool allowBypass;
|
||||
@override
|
||||
final bool systemProxy;
|
||||
@override
|
||||
final int mixedPort;
|
||||
@override
|
||||
final bool onlyProxy;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Props(accessControl: $accessControl, allowBypass: $allowBypass, systemProxy: $systemProxy)';
|
||||
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$PropsImpl &&
|
||||
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) ||
|
||||
other.systemProxy == systemProxy));
|
||||
other.systemProxy == systemProxy) &&
|
||||
(identical(other.mixedPort, mixedPort) ||
|
||||
other.mixedPort == mixedPort) &&
|
||||
(identical(other.onlyProxy, onlyProxy) ||
|
||||
other.onlyProxy == onlyProxy));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, accessControl, allowBypass, systemProxy);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
accessControl,
|
||||
currentProfileName,
|
||||
enable,
|
||||
allowBypass,
|
||||
systemProxy,
|
||||
mixedPort,
|
||||
onlyProxy);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
|
||||
__$$PropsImplCopyWithImpl<_$PropsImpl>(this, _$identity);
|
||||
_$$CoreStateImplCopyWith<_$CoreStateImpl> get copyWith =>
|
||||
__$$CoreStateImplCopyWithImpl<_$CoreStateImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$PropsImplToJson(
|
||||
return _$$CoreStateImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Props implements Props {
|
||||
const factory _Props(
|
||||
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}) = _$PropsImpl;
|
||||
required final bool systemProxy,
|
||||
required final int mixedPort,
|
||||
required final bool onlyProxy}) = _$CoreStateImpl;
|
||||
|
||||
factory _Props.fromJson(Map<String, dynamic> json) = _$PropsImpl.fromJson;
|
||||
factory _CoreState.fromJson(Map<String, dynamic> json) =
|
||||
_$CoreStateImpl.fromJson;
|
||||
|
||||
@override
|
||||
AccessControl? get accessControl;
|
||||
@override
|
||||
String get currentProfileName;
|
||||
@override
|
||||
bool get enable;
|
||||
@override
|
||||
bool get allowBypass;
|
||||
@override
|
||||
bool get systemProxy;
|
||||
@override
|
||||
int get mixedPort;
|
||||
@override
|
||||
bool get onlyProxy;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
|
||||
_$$CoreStateImplCopyWith<_$CoreStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
@@ -621,3 +742,318 @@ 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;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ 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 =
|
||||
@@ -33,20 +36,24 @@ 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? ?? true
|
||||
..onlyProxy = json['onlyProxy'] as bool? ?? false
|
||||
..prueBlack = json['prueBlack'] as bool? ?? false
|
||||
..isCloseConnections = json['isCloseConnections'] as bool? ?? false
|
||||
..proxiesType = $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType'],
|
||||
unknownValue: ProxiesType.tab) ??
|
||||
ProxiesType.tab
|
||||
..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>?);
|
||||
WindowProps.fromJson(json['windowProps'] as Map<String, dynamic>?)
|
||||
..vpnProps = VpnProps.fromJson(json['vpnProps'] as Map<String, dynamic>?)
|
||||
..desktopProps =
|
||||
DesktopProps.fromJson(json['desktopProps'] as Map<String, dynamic>?)
|
||||
..showLabel = json['showLabel'] as bool? ?? false;
|
||||
|
||||
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
|
||||
'profiles': instance.profiles,
|
||||
@@ -59,6 +66,7 @@ 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,
|
||||
@@ -66,14 +74,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 = {
|
||||
@@ -88,6 +99,12 @@ const _$ProxiesSortTypeEnumMap = {
|
||||
ProxiesSortType.name: 'name',
|
||||
};
|
||||
|
||||
const _$ProxiesLayoutEnumMap = {
|
||||
ProxiesLayout.loose: 'loose',
|
||||
ProxiesLayout.standard: 'standard',
|
||||
ProxiesLayout.tight: 'tight',
|
||||
};
|
||||
|
||||
const _$ProxiesTypeEnumMap = {
|
||||
ProxiesType.tab: 'tab',
|
||||
ProxiesType.list: 'list',
|
||||
@@ -111,6 +128,8 @@ _$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,
|
||||
);
|
||||
|
||||
@@ -119,6 +138,7 @@ Map<String, dynamic> _$$AccessControlImplToJson(_$AccessControlImpl instance) =>
|
||||
'mode': _$AccessControlModeEnumMap[instance.mode]!,
|
||||
'acceptList': instance.acceptList,
|
||||
'rejectList': instance.rejectList,
|
||||
'sort': _$AccessSortTypeEnumMap[instance.sort]!,
|
||||
'isFilterSystemApp': instance.isFilterSystemApp,
|
||||
};
|
||||
|
||||
@@ -127,20 +147,35 @@ const _$AccessControlModeEnumMap = {
|
||||
AccessControlMode.rejectSelected: 'rejectSelected',
|
||||
};
|
||||
|
||||
_$PropsImpl _$$PropsImplFromJson(Map<String, dynamic> json) => _$PropsImpl(
|
||||
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(),
|
||||
onlyProxy: json['onlyProxy'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$PropsImplToJson(_$PropsImpl instance) =>
|
||||
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,
|
||||
'onlyProxy': instance.onlyProxy,
|
||||
};
|
||||
|
||||
_$WindowPropsImpl _$$WindowPropsImplFromJson(Map<String, dynamic> json) =>
|
||||
@@ -158,3 +193,27 @@ 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,
|
||||
};
|
||||
|
||||
@@ -248,8 +248,8 @@ UpdateConfigParams _$UpdateConfigParamsFromJson(Map<String, dynamic> json) {
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UpdateConfigParams {
|
||||
@JsonKey(name: "profile-path")
|
||||
String? get profilePath => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "profile-id")
|
||||
String get profileId => 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-path") String? profilePath,
|
||||
{@JsonKey(name: "profile-id") String profileId,
|
||||
ClashConfig config,
|
||||
ConfigExtendedParams params});
|
||||
|
||||
@@ -286,15 +286,15 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? profilePath = freezed,
|
||||
Object? profileId = null,
|
||||
Object? config = null,
|
||||
Object? params = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
profilePath: freezed == profilePath
|
||||
? _value.profilePath
|
||||
: profilePath // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
profileId: null == profileId
|
||||
? _value.profileId
|
||||
: profileId // 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-path") String? profilePath,
|
||||
{@JsonKey(name: "profile-id") String profileId,
|
||||
ClashConfig config,
|
||||
ConfigExtendedParams params});
|
||||
|
||||
@@ -343,15 +343,15 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? profilePath = freezed,
|
||||
Object? profileId = null,
|
||||
Object? config = null,
|
||||
Object? params = null,
|
||||
}) {
|
||||
return _then(_$UpdateConfigParamsImpl(
|
||||
profilePath: freezed == profilePath
|
||||
? _value.profilePath
|
||||
: profilePath // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
profileId: null == profileId
|
||||
? _value.profileId
|
||||
: profileId // 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-path") this.profilePath,
|
||||
{@JsonKey(name: "profile-id") required this.profileId,
|
||||
required this.config,
|
||||
required this.params});
|
||||
|
||||
@@ -376,8 +376,8 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
|
||||
_$$UpdateConfigParamsImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey(name: "profile-path")
|
||||
final String? profilePath;
|
||||
@JsonKey(name: "profile-id")
|
||||
final String profileId;
|
||||
@override
|
||||
final ClashConfig config;
|
||||
@override
|
||||
@@ -385,7 +385,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, params: $params)';
|
||||
return 'UpdateConfigParams(profileId: $profileId, 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.profilePath, profilePath) ||
|
||||
other.profilePath == profilePath) &&
|
||||
(identical(other.profileId, profileId) ||
|
||||
other.profileId == profileId) &&
|
||||
(identical(other.config, config) || other.config == config) &&
|
||||
(identical(other.params, params) || other.params == params));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, profilePath, config, params);
|
||||
int get hashCode => Object.hash(runtimeType, profileId, 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-path") final String? profilePath,
|
||||
{@JsonKey(name: "profile-id") required final String profileId,
|
||||
required final ClashConfig config,
|
||||
required final ConfigExtendedParams params}) = _$UpdateConfigParamsImpl;
|
||||
|
||||
@@ -428,8 +428,8 @@ abstract class _UpdateConfigParams implements UpdateConfigParams {
|
||||
_$UpdateConfigParamsImpl.fromJson;
|
||||
|
||||
@override
|
||||
@JsonKey(name: "profile-path")
|
||||
String? get profilePath;
|
||||
@JsonKey(name: "profile-id")
|
||||
String get profileId;
|
||||
@override
|
||||
ClashConfig get config;
|
||||
@override
|
||||
@@ -1687,6 +1687,9 @@ 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")
|
||||
@@ -1707,6 +1710,9 @@ 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});
|
||||
}
|
||||
@@ -1726,6 +1732,9 @@ 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,
|
||||
}) {
|
||||
@@ -1738,6 +1747,18 @@ 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
|
||||
@@ -1761,6 +1782,9 @@ 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});
|
||||
}
|
||||
@@ -1778,6 +1802,9 @@ 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,
|
||||
}) {
|
||||
@@ -1790,6 +1817,18 @@ 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
|
||||
@@ -1808,6 +1847,9 @@ 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});
|
||||
|
||||
@@ -1819,6 +1861,13 @@ 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
|
||||
@@ -1827,7 +1876,7 @@ class _$ExternalProviderImpl implements _ExternalProvider {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ExternalProvider(name: $name, type: $type, vehicleType: $vehicleType, updateAt: $updateAt)';
|
||||
return 'ExternalProvider(name: $name, type: $type, path: $path, count: $count, isUpdating: $isUpdating, vehicleType: $vehicleType, updateAt: $updateAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1837,6 +1886,10 @@ 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) ||
|
||||
@@ -1845,8 +1898,8 @@ class _$ExternalProviderImpl implements _ExternalProvider {
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, name, type, vehicleType, updateAt);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, name, type, path, count, isUpdating, vehicleType, updateAt);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@@ -1867,6 +1920,9 @@ 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;
|
||||
@@ -1879,6 +1935,12 @@ 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
|
||||
|
||||
@@ -27,7 +27,7 @@ Map<String, dynamic> _$$ConfigExtendedParamsImplToJson(
|
||||
_$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$UpdateConfigParamsImpl(
|
||||
profilePath: json['profile-path'] as String?,
|
||||
profileId: json['profile-id'] 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-path': instance.profilePath,
|
||||
'profile-id': instance.profileId,
|
||||
'config': instance.config,
|
||||
'params': instance.params,
|
||||
};
|
||||
@@ -156,6 +156,9 @@ _$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),
|
||||
);
|
||||
@@ -165,6 +168,9 @@ 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(),
|
||||
};
|
||||
|
||||
150
lib/models/generated/file.freezed.dart
Normal file
150
lib/models/generated/file.freezed.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of '../file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$FileInfo {
|
||||
int get size => throw _privateConstructorUsedError;
|
||||
DateTime get lastModified => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$FileInfoCopyWith<FileInfo> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $FileInfoCopyWith<$Res> {
|
||||
factory $FileInfoCopyWith(FileInfo value, $Res Function(FileInfo) then) =
|
||||
_$FileInfoCopyWithImpl<$Res, FileInfo>;
|
||||
@useResult
|
||||
$Res call({int size, DateTime lastModified});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$FileInfoCopyWithImpl<$Res, $Val extends FileInfo>
|
||||
implements $FileInfoCopyWith<$Res> {
|
||||
_$FileInfoCopyWithImpl(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? size = null,
|
||||
Object? lastModified = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
size: null == size
|
||||
? _value.size
|
||||
: size // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
lastModified: null == lastModified
|
||||
? _value.lastModified
|
||||
: lastModified // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$FileInfoImplCopyWith<$Res>
|
||||
implements $FileInfoCopyWith<$Res> {
|
||||
factory _$$FileInfoImplCopyWith(
|
||||
_$FileInfoImpl value, $Res Function(_$FileInfoImpl) then) =
|
||||
__$$FileInfoImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({int size, DateTime lastModified});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$FileInfoImplCopyWithImpl<$Res>
|
||||
extends _$FileInfoCopyWithImpl<$Res, _$FileInfoImpl>
|
||||
implements _$$FileInfoImplCopyWith<$Res> {
|
||||
__$$FileInfoImplCopyWithImpl(
|
||||
_$FileInfoImpl _value, $Res Function(_$FileInfoImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? size = null,
|
||||
Object? lastModified = null,
|
||||
}) {
|
||||
return _then(_$FileInfoImpl(
|
||||
size: null == size
|
||||
? _value.size
|
||||
: size // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
lastModified: null == lastModified
|
||||
? _value.lastModified
|
||||
: lastModified // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$FileInfoImpl implements _FileInfo {
|
||||
const _$FileInfoImpl({required this.size, required this.lastModified});
|
||||
|
||||
@override
|
||||
final int size;
|
||||
@override
|
||||
final DateTime lastModified;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FileInfo(size: $size, lastModified: $lastModified)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$FileInfoImpl &&
|
||||
(identical(other.size, size) || other.size == size) &&
|
||||
(identical(other.lastModified, lastModified) ||
|
||||
other.lastModified == lastModified));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, size, lastModified);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$FileInfoImplCopyWith<_$FileInfoImpl> get copyWith =>
|
||||
__$$FileInfoImplCopyWithImpl<_$FileInfoImpl>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _FileInfo implements FileInfo {
|
||||
const factory _FileInfo(
|
||||
{required final int size,
|
||||
required final DateTime lastModified}) = _$FileInfoImpl;
|
||||
|
||||
@override
|
||||
int get size;
|
||||
@override
|
||||
DateTime get lastModified;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$FileInfoImplCopyWith<_$FileInfoImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -23,6 +23,7 @@ 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)
|
||||
@@ -34,7 +35,8 @@ abstract class $PackageCopyWith<$Res> {
|
||||
factory $PackageCopyWith(Package value, $Res Function(Package) then) =
|
||||
_$PackageCopyWithImpl<$Res, Package>;
|
||||
@useResult
|
||||
$Res call({String packageName, String label, bool isSystem});
|
||||
$Res call(
|
||||
{String packageName, String label, bool isSystem, int firstInstallTime});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -53,6 +55,7 @@ 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
|
||||
@@ -67,6 +70,10 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +85,8 @@ abstract class _$$PackageImplCopyWith<$Res> implements $PackageCopyWith<$Res> {
|
||||
__$$PackageImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String packageName, String label, bool isSystem});
|
||||
$Res call(
|
||||
{String packageName, String label, bool isSystem, int firstInstallTime});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -95,6 +103,7 @@ class __$$PackageImplCopyWithImpl<$Res>
|
||||
Object? packageName = null,
|
||||
Object? label = null,
|
||||
Object? isSystem = null,
|
||||
Object? firstInstallTime = null,
|
||||
}) {
|
||||
return _then(_$PackageImpl(
|
||||
packageName: null == packageName
|
||||
@@ -109,6 +118,10 @@ 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -117,7 +130,10 @@ class __$$PackageImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$PackageImpl implements _Package {
|
||||
const _$PackageImpl(
|
||||
{required this.packageName, required this.label, required this.isSystem});
|
||||
{required this.packageName,
|
||||
required this.label,
|
||||
required this.isSystem,
|
||||
required this.firstInstallTime});
|
||||
|
||||
factory _$PackageImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$PackageImplFromJson(json);
|
||||
@@ -128,10 +144,12 @@ 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)';
|
||||
return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem, firstInstallTime: $firstInstallTime)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -143,12 +161,15 @@ class _$PackageImpl implements _Package {
|
||||
other.packageName == packageName) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.isSystem, isSystem) ||
|
||||
other.isSystem == isSystem));
|
||||
other.isSystem == isSystem) &&
|
||||
(identical(other.firstInstallTime, firstInstallTime) ||
|
||||
other.firstInstallTime == firstInstallTime));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, packageName, label, isSystem);
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, packageName, label, isSystem, firstInstallTime);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@@ -168,7 +189,8 @@ abstract class _Package implements Package {
|
||||
const factory _Package(
|
||||
{required final String packageName,
|
||||
required final String label,
|
||||
required final bool isSystem}) = _$PackageImpl;
|
||||
required final bool isSystem,
|
||||
required final int firstInstallTime}) = _$PackageImpl;
|
||||
|
||||
factory _Package.fromJson(Map<String, dynamic> json) = _$PackageImpl.fromJson;
|
||||
|
||||
@@ -179,6 +201,8 @@ abstract class _Package implements Package {
|
||||
@override
|
||||
bool get isSystem;
|
||||
@override
|
||||
int get firstInstallTime;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$PackageImplCopyWith<_$PackageImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
@@ -11,6 +11,7 @@ _$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) =>
|
||||
@@ -18,4 +19,5 @@ Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) =>
|
||||
'packageName': instance.packageName,
|
||||
'label': instance.label,
|
||||
'isSystem': instance.isSystem,
|
||||
'firstInstallTime': instance.firstInstallTime,
|
||||
};
|
||||
|
||||
@@ -223,6 +223,8 @@ 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)
|
||||
@@ -244,7 +246,8 @@ abstract class $ProfileCopyWith<$Res> {
|
||||
UserInfo? userInfo,
|
||||
bool autoUpdate,
|
||||
Map<String, String> selectedMap,
|
||||
Set<String> unfoldSet});
|
||||
Set<String> unfoldSet,
|
||||
@JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating});
|
||||
|
||||
$UserInfoCopyWith<$Res>? get userInfo;
|
||||
}
|
||||
@@ -272,6 +275,7 @@ 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
|
||||
@@ -314,6 +318,10 @@ 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);
|
||||
}
|
||||
|
||||
@@ -347,7 +355,8 @@ abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> {
|
||||
UserInfo? userInfo,
|
||||
bool autoUpdate,
|
||||
Map<String, String> selectedMap,
|
||||
Set<String> unfoldSet});
|
||||
Set<String> unfoldSet,
|
||||
@JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating});
|
||||
|
||||
@override
|
||||
$UserInfoCopyWith<$Res>? get userInfo;
|
||||
@@ -374,6 +383,7 @@ class __$$ProfileImplCopyWithImpl<$Res>
|
||||
Object? autoUpdate = null,
|
||||
Object? selectedMap = null,
|
||||
Object? unfoldSet = null,
|
||||
Object? isUpdating = null,
|
||||
}) {
|
||||
return _then(_$ProfileImpl(
|
||||
id: null == id
|
||||
@@ -416,6 +426,10 @@ 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -433,7 +447,9 @@ class _$ProfileImpl implements _Profile {
|
||||
this.userInfo,
|
||||
this.autoUpdate = true,
|
||||
final Map<String, String> selectedMap = const {},
|
||||
final Set<String> unfoldSet = const {}})
|
||||
final Set<String> unfoldSet = const {},
|
||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||
this.isUpdating = false})
|
||||
: _selectedMap = selectedMap,
|
||||
_unfoldSet = unfoldSet;
|
||||
|
||||
@@ -476,9 +492,13 @@ 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)';
|
||||
return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet, isUpdating: $isUpdating)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -502,7 +522,9 @@ class _$ProfileImpl implements _Profile {
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._selectedMap, _selectedMap) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._unfoldSet, _unfoldSet));
|
||||
.equals(other._unfoldSet, _unfoldSet) &&
|
||||
(identical(other.isUpdating, isUpdating) ||
|
||||
other.isUpdating == isUpdating));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@@ -518,7 +540,8 @@ class _$ProfileImpl implements _Profile {
|
||||
userInfo,
|
||||
autoUpdate,
|
||||
const DeepCollectionEquality().hash(_selectedMap),
|
||||
const DeepCollectionEquality().hash(_unfoldSet));
|
||||
const DeepCollectionEquality().hash(_unfoldSet),
|
||||
isUpdating);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@@ -545,7 +568,9 @@ abstract class _Profile implements Profile {
|
||||
final UserInfo? userInfo,
|
||||
final bool autoUpdate,
|
||||
final Map<String, String> selectedMap,
|
||||
final Set<String> unfoldSet}) = _$ProfileImpl;
|
||||
final Set<String> unfoldSet,
|
||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||
final bool isUpdating}) = _$ProfileImpl;
|
||||
|
||||
factory _Profile.fromJson(Map<String, dynamic> json) = _$ProfileImpl.fromJson;
|
||||
|
||||
@@ -570,6 +595,9 @@ 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;
|
||||
|
||||
@@ -23,6 +23,7 @@ mixin _$Group {
|
||||
GroupType get type => throw _privateConstructorUsedError;
|
||||
List<Proxy> get all => throw _privateConstructorUsedError;
|
||||
String? get now => throw _privateConstructorUsedError;
|
||||
bool? get hidden => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -35,7 +36,12 @@ abstract class $GroupCopyWith<$Res> {
|
||||
factory $GroupCopyWith(Group value, $Res Function(Group) then) =
|
||||
_$GroupCopyWithImpl<$Res, Group>;
|
||||
@useResult
|
||||
$Res call({GroupType type, List<Proxy> all, String? now, String name});
|
||||
$Res call(
|
||||
{GroupType type,
|
||||
List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
String name});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -54,6 +60,7 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group>
|
||||
Object? type = null,
|
||||
Object? all = null,
|
||||
Object? now = freezed,
|
||||
Object? hidden = freezed,
|
||||
Object? name = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
@@ -69,6 +76,10 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group>
|
||||
? _value.now
|
||||
: now // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
hidden: freezed == hidden
|
||||
? _value.hidden
|
||||
: hidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
@@ -84,7 +95,12 @@ abstract class _$$GroupImplCopyWith<$Res> implements $GroupCopyWith<$Res> {
|
||||
__$$GroupImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({GroupType type, List<Proxy> all, String? now, String name});
|
||||
$Res call(
|
||||
{GroupType type,
|
||||
List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
String name});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -101,6 +117,7 @@ class __$$GroupImplCopyWithImpl<$Res>
|
||||
Object? type = null,
|
||||
Object? all = null,
|
||||
Object? now = freezed,
|
||||
Object? hidden = freezed,
|
||||
Object? name = null,
|
||||
}) {
|
||||
return _then(_$GroupImpl(
|
||||
@@ -116,6 +133,10 @@ class __$$GroupImplCopyWithImpl<$Res>
|
||||
? _value.now
|
||||
: now // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
hidden: freezed == hidden
|
||||
? _value.hidden
|
||||
: hidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
@@ -131,6 +152,7 @@ class _$GroupImpl implements _Group {
|
||||
{required this.type,
|
||||
final List<Proxy> all = const [],
|
||||
this.now,
|
||||
this.hidden,
|
||||
required this.name})
|
||||
: _all = all;
|
||||
|
||||
@@ -151,11 +173,13 @@ class _$GroupImpl implements _Group {
|
||||
@override
|
||||
final String? now;
|
||||
@override
|
||||
final bool? hidden;
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Group(type: $type, all: $all, now: $now, name: $name)';
|
||||
return 'Group(type: $type, all: $all, now: $now, hidden: $hidden, name: $name)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -166,13 +190,14 @@ class _$GroupImpl implements _Group {
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
const DeepCollectionEquality().equals(other._all, _all) &&
|
||||
(identical(other.now, now) || other.now == now) &&
|
||||
(identical(other.hidden, hidden) || other.hidden == hidden) &&
|
||||
(identical(other.name, name) || other.name == name));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, type, const DeepCollectionEquality().hash(_all), now, name);
|
||||
int get hashCode => Object.hash(runtimeType, type,
|
||||
const DeepCollectionEquality().hash(_all), now, hidden, name);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@@ -193,6 +218,7 @@ abstract class _Group implements Group {
|
||||
{required final GroupType type,
|
||||
final List<Proxy> all,
|
||||
final String? now,
|
||||
final bool? hidden,
|
||||
required final String name}) = _$GroupImpl;
|
||||
|
||||
factory _Group.fromJson(Map<String, dynamic> json) = _$GroupImpl.fromJson;
|
||||
@@ -204,6 +230,8 @@ abstract class _Group implements Group {
|
||||
@override
|
||||
String? get now;
|
||||
@override
|
||||
bool? get hidden;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
|
||||
@@ -13,6 +13,7 @@ _$GroupImpl _$$GroupImplFromJson(Map<String, dynamic> json) => _$GroupImpl(
|
||||
.toList() ??
|
||||
const [],
|
||||
now: json['now'] as String?,
|
||||
hidden: json['hidden'] as bool?,
|
||||
name: json['name'] as String,
|
||||
);
|
||||
|
||||
@@ -21,6 +22,7 @@ Map<String, dynamic> _$$GroupImplToJson(_$GroupImpl instance) =>
|
||||
'type': _$GroupTypeEnumMap[instance.type]!,
|
||||
'all': instance.all,
|
||||
'now': instance.now,
|
||||
'hidden': instance.hidden,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user