Compare commits
71 Commits
v0.8.16-de
...
v0.8.20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86572cc960 | ||
|
|
ee22709d49 | ||
|
|
0a2ad63f38 | ||
|
|
2ec12c9363 | ||
|
|
a3c2dc786c | ||
|
|
7acf9c6db3 | ||
|
|
8074547fb4 | ||
|
|
8a01e04871 | ||
|
|
7ddcdd9828 | ||
|
|
d89ed076fd | ||
|
|
f4c3b06cd5 | ||
|
|
c65746709d | ||
|
|
d2d9bdab02 | ||
|
|
d0f8444b6d | ||
|
|
fccabfbe27 | ||
|
|
e9bb97c6ce | ||
|
|
068fe14d89 | ||
|
|
43c397007c | ||
|
|
5e801d5f5e | ||
|
|
52d61b15fd | ||
|
|
fea3c14608 | ||
|
|
84be01a38a | ||
|
|
93da242148 | ||
|
|
bb7e44da30 | ||
|
|
01f1b2d72f | ||
|
|
3074b1299e | ||
|
|
bd5470b863 | ||
|
|
83fafa4b68 | ||
|
|
b7fb969301 | ||
|
|
3ef3190785 | ||
|
|
b1c763fcfa | ||
|
|
9452ffa514 | ||
|
|
c9291e5027 | ||
|
|
3ba86dc9c2 | ||
|
|
fd3040283c | ||
|
|
91d30c0f0e | ||
|
|
c4b470ffaf | ||
|
|
9a07c785f2 | ||
|
|
a134c32493 | ||
|
|
472cea9037 | ||
|
|
08d07498b9 | ||
|
|
d5aa09949a | ||
|
|
fd1dfe5c60 | ||
|
|
9f89fe8b29 | ||
|
|
78081a12e8 | ||
|
|
6896837f28 | ||
|
|
85eb903402 | ||
|
|
9aa9180f1f | ||
|
|
feb9688a29 | ||
|
|
5c71992174 | ||
|
|
74c3d0ae25 | ||
|
|
ecd1bcafd5 | ||
|
|
184d2d117a | ||
|
|
89e6f17794 | ||
|
|
aef50fe0e3 | ||
|
|
fc0767ed25 | ||
|
|
dbf1724cca | ||
|
|
909aa4038e | ||
|
|
2d0a7d8d46 | ||
|
|
ca96cd1d82 | ||
|
|
91ab1e5dac | ||
|
|
b3a5f74df8 | ||
|
|
1f98be8ad8 | ||
|
|
453c7c98d0 | ||
|
|
91faed35c0 | ||
|
|
07bbaf6b6f | ||
|
|
e8feb7c431 | ||
|
|
4d16820526 | ||
|
|
92294b49c6 | ||
|
|
8a188a37c9 | ||
|
|
48af16c265 |
10
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -21,6 +21,12 @@ jobs:
|
|||||||
os: macos-13
|
os: macos-13
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check Matrix
|
||||||
|
run: |
|
||||||
|
echo "Running on ${{ matrix.os }}"
|
||||||
|
echo "Arch: ${{ runner.arch }}"
|
||||||
|
gcc --version
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -82,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
upload-release:
|
upload-release:
|
||||||
if: ${{ !endsWith(github.ref, '-debug') }}
|
if: ${{ !contains(github.ref, '+') }}
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
needs: [ build ]
|
needs: [ build ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
2
.gitmodules
vendored
@@ -5,4 +5,4 @@
|
|||||||
[submodule "plugins/flutter_distributor"]
|
[submodule "plugins/flutter_distributor"]
|
||||||
path = plugins/flutter_distributor
|
path = plugins/flutter_distributor
|
||||||
url = git@github.com:chen08209/flutter_distributor.git
|
url = git@github.com:chen08209/flutter_distributor.git
|
||||||
branch = main
|
branch = FlClash
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ on Mobile:
|
|||||||
|
|
||||||
💡 Based on Material You Design, [Surfboard](https://github.com/getsurfboard/surfboard)-like UI
|
💡 Based on Material You Design, [Surfboard](https://github.com/getsurfboard/surfboard)-like UI
|
||||||
|
|
||||||
|
☁️ Supports data sync via WebDAV
|
||||||
|
|
||||||
✨ Support subscription link, Dark mode
|
✨ Support subscription link, Dark mode
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ on Mobile:
|
|||||||
|
|
||||||
💡 基本 Material You 设计, 类[Surfboard](https://github.com/getsurfboard/surfboard)用户界面
|
💡 基本 Material You 设计, 类[Surfboard](https://github.com/getsurfboard/surfboard)用户界面
|
||||||
|
|
||||||
|
☁️ 支持通过WebDAV同步数据
|
||||||
|
|
||||||
✨ 支持一键导入订阅, 深色模式
|
✨ 支持一键导入订阅, 深色模式
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.follow.clash"
|
applicationId "com.follow.clash"
|
||||||
minSdkVersion 21
|
minSdkVersion 24
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.software.leanback"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
@@ -17,12 +14,15 @@
|
|||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission"/>
|
tools:ignore="SystemPermissionTypo" />
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:label="FlClash">
|
android:label="FlClash">
|
||||||
<activity
|
<activity
|
||||||
@@ -73,7 +73,8 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".services.FlClashTileService"
|
android:name=".services.FlClashTileService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/tile_icon"
|
android:icon="@drawable/icon"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:label="FlClash"
|
android:label="FlClash"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -10,3 +10,8 @@ data class AccessControl(
|
|||||||
val acceptList: List<String>,
|
val acceptList: List<String>,
|
||||||
val rejectList: List<String>,
|
val rejectList: List<String>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class Props (
|
||||||
|
val accessControl: AccessControl?,
|
||||||
|
val allowBypass: Boolean?,
|
||||||
|
)
|
||||||
@@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.RunState
|
import com.follow.clash.RunState
|
||||||
import com.follow.clash.models.AccessControl
|
import com.follow.clash.models.AccessControl
|
||||||
|
import com.follow.clash.models.Props
|
||||||
import com.follow.clash.services.FlClashVpnService
|
import com.follow.clash.services.FlClashVpnService
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
@@ -41,7 +42,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
|
|||||||
private var flClashVpnService: FlClashVpnService? = null
|
private var flClashVpnService: FlClashVpnService? = null
|
||||||
private var isBound = false
|
private var isBound = false
|
||||||
private var port: Int? = null
|
private var port: Int? = null
|
||||||
private var accessControl: AccessControl? = null
|
private var props: Props? = null
|
||||||
private lateinit var title: String
|
private lateinit var title: String
|
||||||
private lateinit var content: String
|
private lateinit var content: String
|
||||||
|
|
||||||
@@ -73,8 +74,8 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
|
|||||||
"StartProxy" -> {
|
"StartProxy" -> {
|
||||||
port = call.argument<Int>("port")
|
port = call.argument<Int>("port")
|
||||||
val args = call.argument<String>("args")
|
val args = call.argument<String>("args")
|
||||||
accessControl =
|
props =
|
||||||
if (args != null) Gson().fromJson(args, AccessControl::class.java) else null
|
if (args != null) Gson().fromJson(args, Props::class.java) else null
|
||||||
handleStartVpn()
|
handleStartVpn()
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
|
|||||||
|
|
||||||
private fun startVpn(port: Int) {
|
private fun startVpn(port: Int) {
|
||||||
if (GlobalState.runState.value == RunState.START) return;
|
if (GlobalState.runState.value == RunState.START) return;
|
||||||
flClashVpnService?.start(port, accessControl)
|
flClashVpnService?.start(port, props)
|
||||||
GlobalState.runState.value = RunState.START
|
GlobalState.runState.value = RunState.START
|
||||||
GlobalState.runTime = Date()
|
GlobalState.runTime = Date()
|
||||||
startAfter()
|
startAfter()
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import io.flutter.embedding.engine.FlutterEngine
|
|||||||
import io.flutter.embedding.engine.dart.DartExecutor
|
import io.flutter.embedding.engine.dart.DartExecutor
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
|
||||||
class FlClashTileService : TileService() {
|
class FlClashTileService : TileService() {
|
||||||
|
|
||||||
private val observer = Observer<RunState> { runState ->
|
private val observer = Observer<RunState> { runState ->
|
||||||
@@ -43,19 +42,27 @@ class FlClashTileService : TileService() {
|
|||||||
GlobalState.runState.observeForever(observer)
|
GlobalState.runState.observeForever(observer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
|
||||||
private fun activityTransfer() {
|
private fun activityTransfer() {
|
||||||
val intent = Intent(this, TempActivity::class.java)
|
val intent = Intent(this, TempActivity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
if (Build.VERSION.SDK_INT >= 34) {
|
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
startActivityAndCollapse(pendingIntent)
|
|
||||||
} else {
|
} else {
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
startActivityAndCollapse(pendingIntent)
|
||||||
|
}else{
|
||||||
startActivityAndCollapse(intent)
|
startActivityAndCollapse(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package com.follow.clash.services
|
package com.follow.clash.services
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
import android.net.ProxyInfo
|
import android.net.ProxyInfo
|
||||||
@@ -13,15 +12,14 @@ import android.os.Binder
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.MainActivity
|
import com.follow.clash.MainActivity
|
||||||
|
import com.follow.clash.R
|
||||||
import com.follow.clash.models.AccessControl
|
import com.follow.clash.models.AccessControl
|
||||||
import com.follow.clash.models.AccessControlMode
|
import com.follow.clash.models.AccessControlMode
|
||||||
|
import com.follow.clash.models.Props
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
|
||||||
class FlClashVpnService : VpnService() {
|
class FlClashVpnService : VpnService() {
|
||||||
|
|
||||||
|
|
||||||
@@ -54,12 +52,12 @@ class FlClashVpnService : VpnService() {
|
|||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(port: Int, accessControl: AccessControl?) {
|
fun start(port: Int, props: Props?) {
|
||||||
fd = with(Builder()) {
|
fd = with(Builder()) {
|
||||||
addAddress("172.16.0.1", 30)
|
addAddress("172.16.0.1", 30)
|
||||||
setMtu(9000)
|
setMtu(9000)
|
||||||
addRoute("0.0.0.0", 0)
|
addRoute("0.0.0.0", 0)
|
||||||
if (accessControl != null) {
|
props?.accessControl?.let { accessControl ->
|
||||||
when (accessControl.mode) {
|
when (accessControl.mode) {
|
||||||
AccessControlMode.acceptSelected -> {
|
AccessControlMode.acceptSelected -> {
|
||||||
(accessControl.acceptList + packageName).forEach {
|
(accessControl.acceptList + packageName).forEach {
|
||||||
@@ -80,7 +78,9 @@ class FlClashVpnService : VpnService() {
|
|||||||
if (Build.VERSION.SDK_INT >= 29) {
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
setMetered(false)
|
setMetered(false)
|
||||||
}
|
}
|
||||||
allowBypass()
|
if (props?.allowBypass == true) {
|
||||||
|
allowBypass()
|
||||||
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
setHttpProxy(
|
setHttpProxy(
|
||||||
ProxyInfo.buildDirectProxy(
|
ProxyInfo.buildDirectProxy(
|
||||||
@@ -100,10 +100,10 @@ class FlClashVpnService : VpnService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val notificationBuilder by lazy {
|
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
|
||||||
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
@@ -119,34 +119,34 @@ class FlClashVpnService : VpnService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val icon = IconCompat.createWithResource(this, this.applicationInfo.icon)
|
|
||||||
|
|
||||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
setSmallIcon(R.drawable.ic_stat_name)
|
||||||
setSmallIcon(icon)
|
|
||||||
}
|
|
||||||
setContentTitle("FlClash")
|
setContentTitle("FlClash")
|
||||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
priority = NotificationCompat.PRIORITY_MIN
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||||
|
}
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
setShowWhen(false)
|
setShowWhen(false)
|
||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
|
setAutoCancel(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
|
||||||
fun startForeground(title: String, content: String) {
|
fun startForeground(title: String, content: String) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel =
|
val channel =
|
||||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
manager.createNotificationChannel(channel)
|
manager.createNotificationChannel(channel)
|
||||||
|
channel.setShowBadge(false)
|
||||||
}
|
}
|
||||||
val notification =
|
val notification =
|
||||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
} else {
|
} else {
|
||||||
startForeground(notificationId, notification)
|
startForeground(notificationId, notification)
|
||||||
@@ -155,7 +155,7 @@ class FlClashVpnService : VpnService() {
|
|||||||
|
|
||||||
private fun stopForeground() {
|
private fun stopForeground() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
android/app/src/main/res/drawable-hdpi/ic_stat_name.png
Normal file
|
After Width: | Height: | Size: 763 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_stat_name.png
Normal file
|
After Width: | Height: | Size: 520 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_stat_name.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stat_name.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/drawable/icon.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 105 KiB |
@@ -4,7 +4,6 @@
|
|||||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@mipmap/ic_launcher_foreground</item>
|
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@mipmap/ic_launcher_foreground</item>
|
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|||||||
@@ -321,12 +321,12 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig, compatible bool) {
|
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig, compatible bool) {
|
||||||
targetConfig.ExternalController = ""
|
targetConfig.ExternalController = patchConfig.ExternalController
|
||||||
targetConfig.ExternalUI = ""
|
targetConfig.ExternalUI = ""
|
||||||
targetConfig.Interface = ""
|
targetConfig.Interface = ""
|
||||||
targetConfig.ExternalUIURL = ""
|
targetConfig.ExternalUIURL = ""
|
||||||
targetConfig.GeodataMode = false
|
targetConfig.GeodataMode = false
|
||||||
//targetConfig.IPv6 = patchConfig.IPv6
|
targetConfig.IPv6 = patchConfig.IPv6
|
||||||
targetConfig.LogLevel = patchConfig.LogLevel
|
targetConfig.LogLevel = patchConfig.LogLevel
|
||||||
targetConfig.Port = 0
|
targetConfig.Port = 0
|
||||||
targetConfig.SocksPort = 0
|
targetConfig.SocksPort = 0
|
||||||
@@ -336,8 +336,8 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
|||||||
targetConfig.Mode = patchConfig.Mode
|
targetConfig.Mode = patchConfig.Mode
|
||||||
targetConfig.Tun.Enable = patchConfig.Tun.Enable
|
targetConfig.Tun.Enable = patchConfig.Tun.Enable
|
||||||
targetConfig.Tun.Device = patchConfig.Tun.Device
|
targetConfig.Tun.Device = patchConfig.Tun.Device
|
||||||
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
//targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
||||||
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
//targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
||||||
targetConfig.GeodataLoader = "standard"
|
targetConfig.GeodataLoader = "standard"
|
||||||
targetConfig.Profile.StoreSelected = false
|
targetConfig.Profile.StoreSelected = false
|
||||||
if targetConfig.DNS.Enable == false {
|
if targetConfig.DNS.Enable == false {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module core
|
module core
|
||||||
|
|
||||||
go 1.20
|
go 1.21.0
|
||||||
|
|
||||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class ApplicationState extends State<Application> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
globalState.appController = AppController(context);
|
globalState.appController = AppController(context);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||||
globalState.appController.afterInit();
|
await globalState.appController.init();
|
||||||
globalState.appController.initLink();
|
globalState.appController.initLink();
|
||||||
_updateGroups();
|
_updateGroups();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const configKey = "config";
|
|||||||
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
|
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
|
||||||
const double dialogCommonWidth = 300;
|
const double dialogCommonWidth = 300;
|
||||||
const repository = "chen08209/FlClash";
|
const repository = "chen08209/FlClash";
|
||||||
|
const defaultExternalController = "127.0.0.1:9090";
|
||||||
const maxMobileWidth = 600;
|
const maxMobileWidth = 600;
|
||||||
const maxLaptopWidth = 840;
|
const maxLaptopWidth = 840;
|
||||||
final filter = ImageFilter.blur(
|
final filter = ImageFilter.blur(
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ class Picker {
|
|||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
filePickerResult = await FilePicker.platform.pickFiles(
|
filePickerResult = await FilePicker.platform.pickFiles(
|
||||||
withData: true,
|
withData: true,
|
||||||
type: FileType.custom,
|
allowMultiple: false,
|
||||||
allowedExtensions: ['txt', 'conf'],
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
filePickerResult = await FilePicker.platform.pickFiles(
|
filePickerResult = await FilePicker.platform.pickFiles(
|
||||||
|
|||||||
@@ -256,6 +256,29 @@ class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init() async {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
await globalState.applyProfile(
|
||||||
|
appState: appState,
|
||||||
|
config: config,
|
||||||
|
clashConfig: clashConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await afterInit();
|
||||||
|
}
|
||||||
|
|
||||||
afterInit() async {
|
afterInit() async {
|
||||||
if (config.autoRun) {
|
if (config.autoRun) {
|
||||||
await updateSystemProxy(true);
|
await updateSystemProxy(true);
|
||||||
@@ -265,9 +288,6 @@ class AppController {
|
|||||||
}
|
}
|
||||||
autoUpdateProfiles();
|
autoUpdateProfiles();
|
||||||
updateLogStatus();
|
updateLogStatus();
|
||||||
if (!config.silentLaunch) {
|
|
||||||
window?.show();
|
|
||||||
}
|
|
||||||
autoCheckUpdate();
|
autoCheckUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// ignore_for_file: constant_identifier_names
|
// ignore_for_file: constant_identifier_names
|
||||||
|
|
||||||
enum GroupType { Selector, URLTest, Fallback }
|
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
||||||
|
|
||||||
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
||||||
|
|
||||||
@@ -61,4 +61,4 @@ enum MessageType { log, tun, delay, process, now }
|
|||||||
enum RecoveryOption {
|
enum RecoveryOption {
|
||||||
all,
|
all,
|
||||||
onlyProfiles,
|
onlyProfiles,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import 'package:fl_clash/widgets/widgets.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
extension AccessControlExtension on AccessControl {
|
||||||
|
List<String> get currentList => switch (mode) {
|
||||||
|
AccessControlMode.acceptSelected => acceptList,
|
||||||
|
AccessControlMode.rejectSelected => rejectList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class AccessFragment extends StatefulWidget {
|
class AccessFragment extends StatefulWidget {
|
||||||
const AccessFragment({super.key});
|
const AccessFragment({super.key});
|
||||||
|
|
||||||
@@ -83,136 +90,70 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSelectedAllButton({
|
Widget _buildSearchButton(List<Package> packages) {
|
||||||
required bool isSelectedAll,
|
return IconButton(
|
||||||
required List<String> allValueList,
|
tooltip: appLocalizations.search,
|
||||||
}) {
|
onPressed: () {
|
||||||
return Builder(
|
showSearch(
|
||||||
builder: (context) {
|
context: context,
|
||||||
final tooltip = isSelectedAll
|
delegate: AccessControlSearchDelegate(
|
||||||
? appLocalizations.cancelSelectAll
|
packages: packages,
|
||||||
: appLocalizations.selectAll;
|
),
|
||||||
return IconButton(
|
).then((_) => {setState(() {})});
|
||||||
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),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _actionHeader({
|
_buildSelectedAllButton({
|
||||||
required bool isAccessControl,
|
required bool isAccessControl,
|
||||||
required List<String> valueList,
|
required bool isSelectedAll,
|
||||||
required String describe,
|
required List<String> allValueList,
|
||||||
required List<String> packageNameList,
|
|
||||||
}) {
|
}) {
|
||||||
return AbsorbPointer(
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
absorbing: !isAccessControl,
|
final tooltip = isSelectedAll
|
||||||
child: Padding(
|
? appLocalizations.cancelSelectAll
|
||||||
padding: const EdgeInsets.only(
|
: appLocalizations.selectAll;
|
||||||
top: 4,
|
final commonScaffoldState =
|
||||||
bottom: 4,
|
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||||
left: 16,
|
commonScaffoldState?.floatingActionButton = DisabledMask(
|
||||||
right: 8,
|
status: !isAccessControl,
|
||||||
),
|
child: AbsorbPointer(
|
||||||
child: Row(
|
absorbing: !isAccessControl,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
child: FloatingActionButton (
|
||||||
mainAxisSize: MainAxisSize.max,
|
tooltip: tooltip,
|
||||||
children: [
|
onPressed: () {
|
||||||
Expanded(
|
final config = globalState.appController.config;
|
||||||
child: IntrinsicHeight(
|
final isAccept =
|
||||||
child: Column(
|
config.accessControl.mode == AccessControlMode.acceptSelected;
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
if (isSelectedAll) {
|
||||||
children: [
|
config.accessControl = switch (isAccept) {
|
||||||
Expanded(
|
true => config.accessControl.copyWith(
|
||||||
child: Row(
|
acceptList: [],
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
appLocalizations.selected,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.labelLarge
|
|
||||||
?.copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Flexible(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
"${valueList.length}",
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.labelLarge
|
|
||||||
?.copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Flexible(
|
false => config.accessControl.copyWith(
|
||||||
child: Text(describe),
|
rejectList: [],
|
||||||
)
|
),
|
||||||
],
|
};
|
||||||
),
|
} else {
|
||||||
),
|
config.accessControl = switch (isAccept) {
|
||||||
),
|
true => config.accessControl.copyWith(
|
||||||
Row(
|
acceptList: allValueList,
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
false => config.accessControl.copyWith(
|
||||||
children: [
|
rejectList: allValueList,
|
||||||
Flexible(
|
),
|
||||||
child: _buildSelectedAllButton(
|
};
|
||||||
isSelectedAll: const ListEquality<String>()
|
}
|
||||||
.equals(valueList, packageNameList),
|
},
|
||||||
allValueList: packageNameList,
|
child: isSelectedAll
|
||||||
),
|
? const Icon(Icons.deselect)
|
||||||
),
|
: const Icon(Icons.select_all),
|
||||||
Flexible(child: _buildFilterSystemAppButton()),
|
),
|
||||||
Flexible(child: _buildAppProxyModePopup()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPackageList() {
|
Widget _buildPackageList() {
|
||||||
@@ -252,14 +193,11 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
accessControlMode == AccessControlMode.acceptSelected
|
accessControlMode == AccessControlMode.acceptSelected
|
||||||
? acceptPackages
|
? acceptPackages
|
||||||
: rejectPackages;
|
: rejectPackages;
|
||||||
final currentList =
|
final currentList = accessControl.currentList;
|
||||||
accessControlMode == AccessControlMode.acceptSelected
|
|
||||||
? accessControl.acceptList
|
|
||||||
: accessControl.rejectList;
|
|
||||||
final currentPackages = isFilterSystemApp
|
final currentPackages = isFilterSystemApp
|
||||||
? packages
|
? packages
|
||||||
.where((element) => element.isSystem == false)
|
.where((element) => element.isSystem == false)
|
||||||
.toList()
|
.toList()
|
||||||
: packages;
|
: packages;
|
||||||
final packageNameList =
|
final packageNameList =
|
||||||
currentPackages.map((e) => e.packageName).toList();
|
currentPackages.map((e) => e.packageName).toList();
|
||||||
@@ -268,15 +206,91 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
accessControlMode == AccessControlMode.acceptSelected
|
accessControlMode == AccessControlMode.acceptSelected
|
||||||
? appLocalizations.accessControlAllowDesc
|
? appLocalizations.accessControlAllowDesc
|
||||||
: appLocalizations.accessControlNotAllowDesc;
|
: appLocalizations.accessControlNotAllowDesc;
|
||||||
|
_buildSelectedAllButton(
|
||||||
|
isAccessControl: isAccessControl,
|
||||||
|
isSelectedAll: valueList.length == packageNameList.length,
|
||||||
|
allValueList: packageNameList,
|
||||||
|
);
|
||||||
return DisabledMask(
|
return DisabledMask(
|
||||||
status: !isAccessControl,
|
status: !isAccessControl,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_actionHeader(
|
AbsorbPointer(
|
||||||
isAccessControl: isAccessControl,
|
absorbing: !isAccessControl,
|
||||||
valueList: valueList,
|
child: Padding(
|
||||||
describe: describe,
|
padding: const EdgeInsets.only(
|
||||||
packageNameList: packageNameList,
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
left: 16,
|
||||||
|
right: 8,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
appLocalizations.selected,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Flexible(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
"${valueList.length}",
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(describe),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: _buildSearchButton(currentPackages)),
|
||||||
|
Flexible(child: _buildFilterSystemAppButton()),
|
||||||
|
Flexible(child: _buildAppProxyModePopup()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -429,3 +443,111 @@ class PackageListItem extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AccessControlSearchDelegate extends SearchDelegate {
|
||||||
|
final List<Package> packages;
|
||||||
|
|
||||||
|
AccessControlSearchDelegate({
|
||||||
|
required this.packages,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Package> get _results {
|
||||||
|
final lowQuery = query.toLowerCase();
|
||||||
|
return packages
|
||||||
|
.where(
|
||||||
|
(package) =>
|
||||||
|
package.label.toLowerCase().contains(lowQuery) ||
|
||||||
|
package.packageName.contains(lowQuery),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget>? buildActions(BuildContext context) {
|
||||||
|
return [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
close(context, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
query = '';
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? buildLeading(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
close(context, null);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _packageList(List<Package> packages) {
|
||||||
|
return Selector<Config, PackageListSelectorState>(
|
||||||
|
selector: (_, config) => PackageListSelectorState(
|
||||||
|
accessControl: config.accessControl,
|
||||||
|
isAccessControl: config.isAccessControl,
|
||||||
|
),
|
||||||
|
builder: (context, state, __) {
|
||||||
|
final accessControl = state.accessControl;
|
||||||
|
final isAccessControl = state.isAccessControl;
|
||||||
|
final accessControlMode = accessControl.mode;
|
||||||
|
final currentList = accessControl.currentList;
|
||||||
|
final packageNameList =
|
||||||
|
this.packages.map((e) => e.packageName).toList();
|
||||||
|
final valueList = currentList.intersection(packageNameList);
|
||||||
|
return DisabledMask(
|
||||||
|
status: !isAccessControl,
|
||||||
|
child: 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 buildResults(BuildContext context) {
|
||||||
|
return buildSuggestions(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildSuggestions(BuildContext context) {
|
||||||
|
return _packageList(_results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
@@ -54,8 +56,8 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
|||||||
onTab: () {
|
onTab: () {
|
||||||
_modifyMixedPort(mixedPort);
|
_modifyMixedPort(mixedPort);
|
||||||
},
|
},
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
leading: const Icon(Icons.adjust),
|
leading: const Icon(Icons.adjust_outlined),
|
||||||
title: Text(appLocalizations.proxyPort),
|
title: Text(appLocalizations.proxyPort),
|
||||||
trailing: FilledButton.tonal(
|
trailing: FilledButton.tonal(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -68,6 +70,24 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Selector<ClashConfig, bool>(
|
||||||
|
selector: (_, clashConfig) => clashConfig.ipv6,
|
||||||
|
builder: (_, ipv6, __) {
|
||||||
|
return ListItem.switchItem(
|
||||||
|
leading: const Icon(Icons.water_outlined),
|
||||||
|
title: const Text("Ipv6"),
|
||||||
|
subtitle: Text(appLocalizations.ipv6Desc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: ipv6,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
final appController = globalState.appController;
|
||||||
|
appController.clashConfig.ipv6 = value;
|
||||||
|
appController.updateClashConfigDebounce();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
Selector<ClashConfig, bool>(
|
Selector<ClashConfig, bool>(
|
||||||
selector: (_, clashConfig) => clashConfig.allowLan,
|
selector: (_, clashConfig) => clashConfig.allowLan,
|
||||||
builder: (_, allowLan, __) {
|
builder: (_, allowLan, __) {
|
||||||
@@ -105,11 +125,29 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
|||||||
// );
|
// );
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
|
if (Platform.isAndroid)
|
||||||
|
Selector<Config, bool>(
|
||||||
|
selector: (_, config) => config.allowBypass,
|
||||||
|
builder: (_, allowBypass, __) {
|
||||||
|
return ListItem.switchItem(
|
||||||
|
leading: const Icon(Icons.double_arrow),
|
||||||
|
title: Text(appLocalizations.allowBypass),
|
||||||
|
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: allowBypass,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
final appController = globalState.appController;
|
||||||
|
appController.config.allowBypass = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.isCompatible,
|
selector: (_, config) => config.isCompatible,
|
||||||
builder: (_, isCompatible, __) {
|
builder: (_, isCompatible, __) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.expand),
|
leading: const Icon(Icons.expand_outlined),
|
||||||
title: Text(appLocalizations.compatible),
|
title: Text(appLocalizations.compatible),
|
||||||
subtitle: Text(appLocalizations.compatibleDesc),
|
subtitle: Text(appLocalizations.compatibleDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
@@ -125,25 +163,39 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Selector<ClashConfig, bool>(
|
||||||
|
// selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
|
||||||
|
// builder: (_, hasExternalController, __) {
|
||||||
|
// return ListItem.switchItem(
|
||||||
|
// leading: const Icon(Icons.api_outlined),
|
||||||
|
// title: Text(appLocalizations.externalController),
|
||||||
|
// subtitle: Text(appLocalizations.externalControllerDesc),
|
||||||
|
// delegate: SwitchDelegate(
|
||||||
|
// value: hasExternalController,
|
||||||
|
// onChanged: (bool value) async {
|
||||||
|
// final appController = globalState.appController;
|
||||||
|
// appController.clashConfig.externalController =
|
||||||
|
// value ? defaultExternalController : '';
|
||||||
|
// await appController.updateClashConfig(
|
||||||
|
// isPatch: false,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
Padding(
|
Padding(
|
||||||
padding: kMaterialListPadding,
|
padding: kMaterialListPadding,
|
||||||
child: Selector<ClashConfig, LogLevel>(
|
child: Selector<ClashConfig, LogLevel>(
|
||||||
selector: (_, clashConfig) => clashConfig.logLevel,
|
selector: (_, clashConfig) => clashConfig.logLevel,
|
||||||
builder: (_, value, __) {
|
builder: (_, value, __) {
|
||||||
return ListItem(
|
return ListItem(
|
||||||
leading: const Icon(Icons.feedback),
|
leading: const Icon(Icons.info_outline),
|
||||||
title: Text(appLocalizations.logLevel),
|
title: Text(appLocalizations.logLevel),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
child: DropdownMenu<LogLevel>(
|
child: DropdownMenu<LogLevel>(
|
||||||
width: 124,
|
width: 124,
|
||||||
inputDecorationTheme: const InputDecorationTheme(
|
|
||||||
filled: true,
|
|
||||||
contentPadding: EdgeInsets.symmetric(
|
|
||||||
vertical: 5,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
initialSelection: value,
|
initialSelection: value,
|
||||||
dropdownMenuEntries: [
|
dropdownMenuEntries: [
|
||||||
for (final logLevel in LogLevel.values)
|
for (final logLevel in LogLevel.values)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
bool isStart,
|
bool isStart,
|
||||||
) async {
|
) async {
|
||||||
if (!isInit) return;
|
if (!isInit) return;
|
||||||
|
timeoutNotifier.value = false;
|
||||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||||
if (cancelToken != null) {
|
if (cancelToken != null) {
|
||||||
cancelToken!.cancel();
|
cancelToken!.cancel();
|
||||||
@@ -38,6 +39,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
} else {
|
} else {
|
||||||
timeoutNotifier.value = false;
|
timeoutNotifier.value = false;
|
||||||
}
|
}
|
||||||
|
_preIsStart = isStart;
|
||||||
ipInfoNotifier.value = ipInfo;
|
ipInfoNotifier.value = ipInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,9 +152,10 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
builder: (_, timeout, __) {
|
builder: (_, timeout, __) {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
return Text(
|
return Text(
|
||||||
appLocalizations.ipCheckTimeout,
|
"timeout",
|
||||||
style: context.textTheme.titleLarge
|
style: context.textTheme.titleMedium
|
||||||
?.toSoftBold(),
|
?.copyWith(color: Colors.red)
|
||||||
|
.toSoftBold(),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,9 +17,16 @@ enum ProfileActions {
|
|||||||
delete,
|
delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfilesFragment extends StatelessWidget {
|
class ProfilesFragment extends StatefulWidget {
|
||||||
const ProfilesFragment({super.key});
|
const ProfilesFragment({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfilesFragment> createState() => _ProfilesFragmentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||||
|
final hasPadding = ValueNotifier<bool>(false);
|
||||||
|
|
||||||
_handleShowAddExtendPage() {
|
_handleShowAddExtendPage() {
|
||||||
showExtendPage(
|
showExtendPage(
|
||||||
globalState.navigatorKey.currentState!.context,
|
globalState.navigatorKey.currentState!.context,
|
||||||
@@ -41,7 +48,7 @@ class ProfilesFragment extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initScaffoldState(BuildContext context) {
|
_initScaffoldState() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
(_) {
|
(_) {
|
||||||
final commonScaffoldState =
|
final commonScaffoldState =
|
||||||
@@ -55,7 +62,7 @@ class ProfilesFragment extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.download),
|
icon: const Icon(Icons.sync),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
@@ -78,7 +85,7 @@ class ProfilesFragment extends StatelessWidget {
|
|||||||
selector: (_, appState) => appState.currentLabel == 'profiles',
|
selector: (_, appState) => appState.currentLabel == 'profiles',
|
||||||
builder: (_, isCurrent, child) {
|
builder: (_, isCurrent, child) {
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
_initScaffoldState(context);
|
_initScaffoldState();
|
||||||
}
|
}
|
||||||
return child!;
|
return child!;
|
||||||
},
|
},
|
||||||
@@ -98,27 +105,46 @@ class ProfilesFragment extends StatelessWidget {
|
|||||||
final isMobile = state.viewMode == ViewMode.mobile;
|
final isMobile = state.viewMode == ViewMode.mobile;
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: SingleChildScrollView(
|
child: NotificationListener<ScrollNotification>(
|
||||||
padding: !isMobile
|
onNotification: (scrollNotification) {
|
||||||
? const EdgeInsets.symmetric(
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
horizontal: 16,
|
hasPadding.value =
|
||||||
vertical: 16,
|
scrollNotification.metrics.maxScrollExtent > 0;
|
||||||
)
|
});
|
||||||
: EdgeInsets.zero,
|
return true;
|
||||||
child: Grid(
|
},
|
||||||
mainAxisSpacing: isMobile ? 8 : 16,
|
child: ValueListenableBuilder(
|
||||||
crossAxisSpacing: 16,
|
valueListenable: hasPadding,
|
||||||
crossAxisCount: columns,
|
builder: (_, hasPadding, __) {
|
||||||
children: [
|
return SingleChildScrollView(
|
||||||
for (final profile in state.profiles)
|
padding: !isMobile
|
||||||
GridItem(
|
? EdgeInsets.only(
|
||||||
child: ProfileItem(
|
left: 16,
|
||||||
profile: profile,
|
right: 16,
|
||||||
groupValue: state.currentProfileId,
|
top: 16,
|
||||||
onChanged: globalState.appController.changeProfile,
|
bottom: 16 + (hasPadding ? 56 : 0),
|
||||||
),
|
)
|
||||||
|
: EdgeInsets.only(
|
||||||
|
bottom: 0 + (hasPadding ? 56 : 0),
|
||||||
|
),
|
||||||
|
child: Grid(
|
||||||
|
mainAxisSpacing: isMobile ? 8 : 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
crossAxisCount: columns,
|
||||||
|
children: [
|
||||||
|
for (final profile in state.profiles)
|
||||||
|
GridItem(
|
||||||
|
child: ProfileItem(
|
||||||
|
profile: profile,
|
||||||
|
groupValue: state.currentProfileId,
|
||||||
|
onChanged:
|
||||||
|
globalState.appController.changeProfile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -199,10 +199,15 @@ class ProxiesTabView extends StatelessWidget {
|
|||||||
|
|
||||||
_delayTest(List<Proxy> proxies) async {
|
_delayTest(List<Proxy> proxies) async {
|
||||||
for (final proxy in proxies) {
|
for (final proxy in proxies) {
|
||||||
|
final appController = globalState.appController;
|
||||||
|
final proxyName = appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
|
||||||
globalState.appController.setDelay(
|
globalState.appController.setDelay(
|
||||||
Delay(name: proxy.name, value: 0),
|
Delay(
|
||||||
|
name: proxyName,
|
||||||
|
value: 0,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
clashCore.getDelay(proxy.name).then((delay) {
|
clashCore.getDelay(proxyName).then((delay) {
|
||||||
globalState.appController.setDelay(delay);
|
globalState.appController.setDelay(delay);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -434,7 +439,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
|
|||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(
|
duration: const Duration(
|
||||||
milliseconds: 600,
|
milliseconds: 200,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_scale = Tween<double>(
|
_scale = Tween<double>(
|
||||||
|
|||||||
@@ -160,5 +160,11 @@
|
|||||||
"checking": "Checking...",
|
"checking": "Checking...",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"checkError": "Check error",
|
"checkError": "Check error",
|
||||||
"ipCheckTimeout": "Ip check timeout"
|
"ipCheckTimeout": "Ip check timeout",
|
||||||
|
"search": "Search",
|
||||||
|
"allowBypass": "Allow applications to bypass VPN",
|
||||||
|
"allowBypassDesc": "Enabled to some applications can bypass VPN",
|
||||||
|
"externalController": "ExternalController",
|
||||||
|
"externalControllerDesc": "Enabled to control the clash on port 9090",
|
||||||
|
"ipv6Desc": "Enabled to will allow it to receive ipv6 traffic"
|
||||||
}
|
}
|
||||||
@@ -160,5 +160,11 @@
|
|||||||
"checking": "检测中...",
|
"checking": "检测中...",
|
||||||
"country": "区域",
|
"country": "区域",
|
||||||
"checkError": "检测失败",
|
"checkError": "检测失败",
|
||||||
"ipCheckTimeout": "Ip检测超时"
|
"ipCheckTimeout": "Ip检测超时",
|
||||||
|
"search": "搜索",
|
||||||
|
"allowBypass": "允许应用绕过vpn",
|
||||||
|
"allowBypassDesc": "开启后部分应用可绕过VPN",
|
||||||
|
"externalController": "外部控制器",
|
||||||
|
"externalControllerDesc": "开启后可通过9090端口控制clash内核",
|
||||||
|
"ipv6Desc": "开启后将可以接收ipv6流量"
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"addressTip": MessageLookupByLibrary.simpleMessage(
|
"addressTip": MessageLookupByLibrary.simpleMessage(
|
||||||
"Please enter a valid WebDAV address"),
|
"Please enter a valid WebDAV address"),
|
||||||
"ago": MessageLookupByLibrary.simpleMessage(" Ago"),
|
"ago": MessageLookupByLibrary.simpleMessage(" Ago"),
|
||||||
|
"allowBypass": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Allow applications to bypass VPN"),
|
||||||
|
"allowBypassDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Enabled to some applications can bypass VPN"),
|
||||||
"allowLan": MessageLookupByLibrary.simpleMessage("AllowLan"),
|
"allowLan": MessageLookupByLibrary.simpleMessage("AllowLan"),
|
||||||
"allowLanDesc": MessageLookupByLibrary.simpleMessage(
|
"allowLanDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"Allow access proxy through the LAN"),
|
"Allow access proxy through the LAN"),
|
||||||
@@ -112,6 +116,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
|
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
|
||||||
"en": MessageLookupByLibrary.simpleMessage("English"),
|
"en": MessageLookupByLibrary.simpleMessage("English"),
|
||||||
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
|
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
|
||||||
|
"externalController":
|
||||||
|
MessageLookupByLibrary.simpleMessage("ExternalController"),
|
||||||
|
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Enabled to control the clash on port 9090"),
|
||||||
"externalResources":
|
"externalResources":
|
||||||
MessageLookupByLibrary.simpleMessage("External resources"),
|
MessageLookupByLibrary.simpleMessage("External resources"),
|
||||||
"file": MessageLookupByLibrary.simpleMessage("File"),
|
"file": MessageLookupByLibrary.simpleMessage("File"),
|
||||||
@@ -127,6 +135,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
MessageLookupByLibrary.simpleMessage("Import from URL"),
|
MessageLookupByLibrary.simpleMessage("Import from URL"),
|
||||||
"ipCheckTimeout":
|
"ipCheckTimeout":
|
||||||
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
|
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
|
||||||
|
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Enabled to will allow it to receive ipv6 traffic"),
|
||||||
"just": MessageLookupByLibrary.simpleMessage("Just"),
|
"just": MessageLookupByLibrary.simpleMessage("Just"),
|
||||||
"language": MessageLookupByLibrary.simpleMessage("Language"),
|
"language": MessageLookupByLibrary.simpleMessage("Language"),
|
||||||
"light": MessageLookupByLibrary.simpleMessage("Light"),
|
"light": MessageLookupByLibrary.simpleMessage("Light"),
|
||||||
@@ -212,6 +222,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"External resource related info"),
|
"External resource related info"),
|
||||||
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
|
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
|
||||||
"save": MessageLookupByLibrary.simpleMessage("Save"),
|
"save": MessageLookupByLibrary.simpleMessage("Save"),
|
||||||
|
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||||
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
|
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
|
||||||
"selected": MessageLookupByLibrary.simpleMessage("Selected"),
|
"selected": MessageLookupByLibrary.simpleMessage("Selected"),
|
||||||
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
|
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
|
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
|
||||||
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
|
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
|
||||||
"ago": MessageLookupByLibrary.simpleMessage("前"),
|
"ago": MessageLookupByLibrary.simpleMessage("前"),
|
||||||
|
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过vpn"),
|
||||||
|
"allowBypassDesc":
|
||||||
|
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
|
||||||
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
|
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
|
||||||
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
|
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
|
||||||
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
|
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
|
||||||
@@ -93,6 +96,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
||||||
"en": MessageLookupByLibrary.simpleMessage("英语"),
|
"en": MessageLookupByLibrary.simpleMessage("英语"),
|
||||||
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
||||||
|
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
||||||
|
"externalControllerDesc":
|
||||||
|
MessageLookupByLibrary.simpleMessage("开启后可通过9090端口控制clash内核"),
|
||||||
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
|
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
|
||||||
"file": MessageLookupByLibrary.simpleMessage("文件"),
|
"file": MessageLookupByLibrary.simpleMessage("文件"),
|
||||||
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
|
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
|
||||||
@@ -103,6 +109,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"hours": MessageLookupByLibrary.simpleMessage("小时"),
|
"hours": MessageLookupByLibrary.simpleMessage("小时"),
|
||||||
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
|
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
|
||||||
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
|
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
|
||||||
|
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"),
|
||||||
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
||||||
"language": MessageLookupByLibrary.simpleMessage("语言"),
|
"language": MessageLookupByLibrary.simpleMessage("语言"),
|
||||||
"light": MessageLookupByLibrary.simpleMessage("浅色"),
|
"light": MessageLookupByLibrary.simpleMessage("浅色"),
|
||||||
@@ -171,6 +178,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
|
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
|
||||||
"rule": MessageLookupByLibrary.simpleMessage("规则"),
|
"rule": MessageLookupByLibrary.simpleMessage("规则"),
|
||||||
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
||||||
|
"search": MessageLookupByLibrary.simpleMessage("搜索"),
|
||||||
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
|
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
|
||||||
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
|
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
|
||||||
"settings": MessageLookupByLibrary.simpleMessage("设置"),
|
"settings": MessageLookupByLibrary.simpleMessage("设置"),
|
||||||
|
|||||||
@@ -1669,6 +1669,66 @@ class AppLocalizations {
|
|||||||
args: [],
|
args: [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Search`
|
||||||
|
String get search {
|
||||||
|
return Intl.message(
|
||||||
|
'Search',
|
||||||
|
name: 'search',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Allow applications to bypass VPN`
|
||||||
|
String get allowBypass {
|
||||||
|
return Intl.message(
|
||||||
|
'Allow applications to bypass VPN',
|
||||||
|
name: 'allowBypass',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Enabled to some applications can bypass VPN`
|
||||||
|
String get allowBypassDesc {
|
||||||
|
return Intl.message(
|
||||||
|
'Enabled to some applications can bypass VPN',
|
||||||
|
name: 'allowBypassDesc',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `ExternalController`
|
||||||
|
String get externalController {
|
||||||
|
return Intl.message(
|
||||||
|
'ExternalController',
|
||||||
|
name: 'externalController',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Enabled to control the clash on port 9090`
|
||||||
|
String get externalControllerDesc {
|
||||||
|
return Intl.message(
|
||||||
|
'Enabled to control the clash on port 9090',
|
||||||
|
name: 'externalControllerDesc',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Enabled to will allow it to receive ipv6 traffic`
|
||||||
|
String get ipv6Desc {
|
||||||
|
return Intl.message(
|
||||||
|
'Enabled to will allow it to receive ipv6 traffic',
|
||||||
|
name: 'ipv6Desc',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||||
|
|||||||
@@ -22,16 +22,16 @@ Future<void> main() async {
|
|||||||
isCompatible: config.isCompatible,
|
isCompatible: config.isCompatible,
|
||||||
selectedMap: config.currentSelectedMap,
|
selectedMap: config.currentSelectedMap,
|
||||||
);
|
);
|
||||||
|
appState.navigationItems = navigation.getItems(
|
||||||
|
openLogs: config.openLogs,
|
||||||
|
hasProxies: false,
|
||||||
|
);
|
||||||
await globalState.init(
|
await globalState.init(
|
||||||
appState: appState,
|
appState: appState,
|
||||||
config: config,
|
config: config,
|
||||||
clashConfig: clashConfig,
|
clashConfig: clashConfig,
|
||||||
);
|
);
|
||||||
globalState.updateNavigationItems(
|
|
||||||
appState: appState,
|
|
||||||
config: config,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
);
|
|
||||||
runAppWithPreferences(
|
runAppWithPreferences(
|
||||||
const Application(),
|
const Application(),
|
||||||
appState: appState,
|
appState: appState,
|
||||||
@@ -61,6 +61,16 @@ Future<void> vpnService() async {
|
|||||||
clashConfig: clashConfig,
|
clashConfig: clashConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appState.isInit) {
|
||||||
|
await globalState.applyProfile(
|
||||||
|
appState: appState,
|
||||||
|
config: config,
|
||||||
|
clashConfig: clashConfig,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
final appLocalizations = await AppLocalizations.load(
|
final appLocalizations = await AppLocalizations.load(
|
||||||
other.getLocaleForString(config.locale) ??
|
other.getLocaleForString(config.locale) ??
|
||||||
WidgetsBinding.instance.platformDispatcher.locale,
|
WidgetsBinding.instance.platformDispatcher.locale,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class AppState with ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
final index = groups.indexWhere((element) => element.name == proxyName);
|
final index = groups.indexWhere((element) => element.name == proxyName);
|
||||||
if (index == -1) return type;
|
if (index == -1) return type;
|
||||||
return "$type(${groups[index].now})";
|
return "$type(${groups[index].now ?? '*'})";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Tun with _$Tun {
|
|||||||
const factory Tun({
|
const factory Tun({
|
||||||
@Default(false) bool enable,
|
@Default(false) bool enable,
|
||||||
@Default(appName) String device,
|
@Default(appName) String device,
|
||||||
@Default(TunStack.mixed) TunStack stack,
|
@Default(TunStack.gvisor) TunStack stack,
|
||||||
@JsonKey(name: "dns-hijack") @Default(["any:53"]) List<String> dnsHijack,
|
@JsonKey(name: "dns-hijack") @Default(["any:53"]) List<String> dnsHijack,
|
||||||
}) = _Tun;
|
}) = _Tun;
|
||||||
|
|
||||||
@@ -108,6 +108,8 @@ class Dns {
|
|||||||
class ClashConfig extends ChangeNotifier {
|
class ClashConfig extends ChangeNotifier {
|
||||||
int _mixedPort;
|
int _mixedPort;
|
||||||
bool _allowLan;
|
bool _allowLan;
|
||||||
|
bool _ipv6;
|
||||||
|
String _externalController;
|
||||||
Mode _mode;
|
Mode _mode;
|
||||||
LogLevel _logLevel;
|
LogLevel _logLevel;
|
||||||
Tun _tun;
|
Tun _tun;
|
||||||
@@ -118,15 +120,19 @@ class ClashConfig extends ChangeNotifier {
|
|||||||
int? mixedPort,
|
int? mixedPort,
|
||||||
Mode? mode,
|
Mode? mode,
|
||||||
bool? allowLan,
|
bool? allowLan,
|
||||||
|
bool? ipv6,
|
||||||
LogLevel? logLevel,
|
LogLevel? logLevel,
|
||||||
|
String? externalController,
|
||||||
Tun? tun,
|
Tun? tun,
|
||||||
Dns? dns,
|
Dns? dns,
|
||||||
List<String>? rules,
|
List<String>? rules,
|
||||||
}) : _mixedPort = mixedPort ?? 7890,
|
}) : _mixedPort = mixedPort ?? 7890,
|
||||||
_mode = mode ?? Mode.rule,
|
_mode = mode ?? Mode.rule,
|
||||||
|
_ipv6 = ipv6 ?? false,
|
||||||
_allowLan = allowLan ?? false,
|
_allowLan = allowLan ?? false,
|
||||||
_logLevel = logLevel ?? LogLevel.info,
|
_logLevel = logLevel ?? LogLevel.info,
|
||||||
_tun = tun ?? const Tun(),
|
_tun = tun ?? const Tun(),
|
||||||
|
_externalController = externalController ?? '',
|
||||||
_dns = dns ?? Dns(),
|
_dns = dns ?? Dns(),
|
||||||
_rules = rules ?? [];
|
_rules = rules ?? [];
|
||||||
|
|
||||||
@@ -169,6 +175,26 @@ class ClashConfig extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonKey(name: "external-controller", defaultValue: '')
|
||||||
|
String get externalController => _externalController;
|
||||||
|
|
||||||
|
set externalController(String value) {
|
||||||
|
if (_externalController != value) {
|
||||||
|
_externalController = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
bool get ipv6 => _ipv6;
|
||||||
|
|
||||||
|
set ipv6(bool value) {
|
||||||
|
if (_ipv6 != value) {
|
||||||
|
_ipv6 = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Tun get tun => _tun;
|
Tun get tun => _tun;
|
||||||
|
|
||||||
set tun(Tun value) {
|
set tun(Tun value) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '../common/common.dart';
|
|||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
part 'generated/config.g.dart';
|
part 'generated/config.g.dart';
|
||||||
|
|
||||||
part 'generated/config.freezed.dart';
|
part 'generated/config.freezed.dart';
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -23,6 +24,17 @@ class AccessControl with _$AccessControl {
|
|||||||
_$AccessControlFromJson(json);
|
_$AccessControlFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class Props with _$Props {
|
||||||
|
const factory Props({
|
||||||
|
AccessControl? accessControl,
|
||||||
|
bool? allowBypass,
|
||||||
|
}) = _Props;
|
||||||
|
|
||||||
|
factory Props.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$PropsFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Config extends ChangeNotifier {
|
class Config extends ChangeNotifier {
|
||||||
List<Profile> _profiles;
|
List<Profile> _profiles;
|
||||||
@@ -41,6 +53,7 @@ class Config extends ChangeNotifier {
|
|||||||
AccessControl _accessControl;
|
AccessControl _accessControl;
|
||||||
bool _isAnimateToPage;
|
bool _isAnimateToPage;
|
||||||
bool _autoCheckUpdate;
|
bool _autoCheckUpdate;
|
||||||
|
bool _allowBypass;
|
||||||
DAV? _dav;
|
DAV? _dav;
|
||||||
|
|
||||||
Config()
|
Config()
|
||||||
@@ -57,7 +70,8 @@ class Config extends ChangeNotifier {
|
|||||||
_isAccessControl = false,
|
_isAccessControl = false,
|
||||||
_autoCheckUpdate = true,
|
_autoCheckUpdate = true,
|
||||||
_accessControl = const AccessControl(),
|
_accessControl = const AccessControl(),
|
||||||
_isAnimateToPage = true;
|
_isAnimateToPage = true,
|
||||||
|
_allowBypass = true;
|
||||||
|
|
||||||
deleteProfileById(String id) {
|
deleteProfileById(String id) {
|
||||||
_profiles = profiles.where((element) => element.id != id).toList();
|
_profiles = profiles.where((element) => element.id != id).toList();
|
||||||
@@ -305,8 +319,22 @@ class Config extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
@JsonKey(defaultValue: true)
|
||||||
[Config? config, RecoveryOption recoveryOptions = RecoveryOption.all]) {
|
bool get allowBypass {
|
||||||
|
return _allowBypass;
|
||||||
|
}
|
||||||
|
|
||||||
|
set allowBypass(bool value) {
|
||||||
|
if (_allowBypass != value) {
|
||||||
|
_allowBypass = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update([
|
||||||
|
Config? config,
|
||||||
|
RecoveryOption recoveryOptions = RecoveryOption.all,
|
||||||
|
]) {
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
_profiles = config._profiles;
|
_profiles = config._profiles;
|
||||||
for (final profile in config._profiles) {
|
for (final profile in config._profiles) {
|
||||||
@@ -325,6 +353,7 @@ class Config extends ChangeNotifier {
|
|||||||
_openLog = config._openLog;
|
_openLog = config._openLog;
|
||||||
_themeMode = config._themeMode;
|
_themeMode = config._themeMode;
|
||||||
_locale = config._locale;
|
_locale = config._locale;
|
||||||
|
_allowBypass = config._allowBypass;
|
||||||
_primaryColor = config._primaryColor;
|
_primaryColor = config._primaryColor;
|
||||||
_proxiesSortType = config._proxiesSortType;
|
_proxiesSortType = config._proxiesSortType;
|
||||||
_isMinimizeOnExit = config._isMinimizeOnExit;
|
_isMinimizeOnExit = config._isMinimizeOnExit;
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class _$TunImpl implements _Tun {
|
|||||||
const _$TunImpl(
|
const _$TunImpl(
|
||||||
{this.enable = false,
|
{this.enable = false,
|
||||||
this.device = appName,
|
this.device = appName,
|
||||||
this.stack = TunStack.mixed,
|
this.stack = TunStack.gvisor,
|
||||||
@JsonKey(name: "dns-hijack")
|
@JsonKey(name: "dns-hijack")
|
||||||
final List<String> dnsHijack = const ["any:53"]})
|
final List<String> dnsHijack = const ["any:53"]})
|
||||||
: _dnsHijack = dnsHijack;
|
: _dnsHijack = dnsHijack;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig(
|
|||||||
mode: $enumDecodeNullable(_$ModeEnumMap, json['mode']),
|
mode: $enumDecodeNullable(_$ModeEnumMap, json['mode']),
|
||||||
allowLan: json['allow-lan'] as bool?,
|
allowLan: json['allow-lan'] as bool?,
|
||||||
logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']),
|
logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']),
|
||||||
|
externalController: json['external-controller'] as String? ?? '',
|
||||||
tun: json['tun'] == null
|
tun: json['tun'] == null
|
||||||
? null
|
? null
|
||||||
: Tun.fromJson(json['tun'] as Map<String, dynamic>),
|
: Tun.fromJson(json['tun'] as Map<String, dynamic>),
|
||||||
@@ -56,6 +57,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
|
|||||||
'mode': _$ModeEnumMap[instance.mode]!,
|
'mode': _$ModeEnumMap[instance.mode]!,
|
||||||
'allow-lan': instance.allowLan,
|
'allow-lan': instance.allowLan,
|
||||||
'log-level': _$LogLevelEnumMap[instance.logLevel]!,
|
'log-level': _$LogLevelEnumMap[instance.logLevel]!,
|
||||||
|
'external-controller': instance.externalController,
|
||||||
'tun': instance.tun,
|
'tun': instance.tun,
|
||||||
'dns': instance.dns,
|
'dns': instance.dns,
|
||||||
'rules': instance.rules,
|
'rules': instance.rules,
|
||||||
@@ -79,7 +81,7 @@ _$TunImpl _$$TunImplFromJson(Map<String, dynamic> json) => _$TunImpl(
|
|||||||
enable: json['enable'] as bool? ?? false,
|
enable: json['enable'] as bool? ?? false,
|
||||||
device: json['device'] as String? ?? appName,
|
device: json['device'] as String? ?? appName,
|
||||||
stack: $enumDecodeNullable(_$TunStackEnumMap, json['stack']) ??
|
stack: $enumDecodeNullable(_$TunStackEnumMap, json['stack']) ??
|
||||||
TunStack.mixed,
|
TunStack.gvisor,
|
||||||
dnsHijack: (json['dns-hijack'] as List<dynamic>?)
|
dnsHijack: (json['dns-hijack'] as List<dynamic>?)
|
||||||
?.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
|
|||||||
@@ -239,3 +239,172 @@ abstract class _AccessControl implements AccessControl {
|
|||||||
_$$AccessControlImplCopyWith<_$AccessControlImpl> get copyWith =>
|
_$$AccessControlImplCopyWith<_$AccessControlImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Props _$PropsFromJson(Map<String, dynamic> json) {
|
||||||
|
return _Props.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$Props {
|
||||||
|
AccessControl? get accessControl => throw _privateConstructorUsedError;
|
||||||
|
bool? get allowBypass => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$PropsCopyWith<Props> get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $PropsCopyWith<$Res> {
|
||||||
|
factory $PropsCopyWith(Props value, $Res Function(Props) then) =
|
||||||
|
_$PropsCopyWithImpl<$Res, Props>;
|
||||||
|
@useResult
|
||||||
|
$Res call({AccessControl? accessControl, bool? allowBypass});
|
||||||
|
|
||||||
|
$AccessControlCopyWith<$Res>? get accessControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PropsCopyWithImpl<$Res, $Val extends Props>
|
||||||
|
implements $PropsCopyWith<$Res> {
|
||||||
|
_$PropsCopyWithImpl(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? accessControl = freezed,
|
||||||
|
Object? allowBypass = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
accessControl: freezed == accessControl
|
||||||
|
? _value.accessControl
|
||||||
|
: accessControl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AccessControl?,
|
||||||
|
allowBypass: freezed == allowBypass
|
||||||
|
? _value.allowBypass
|
||||||
|
: allowBypass // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$AccessControlCopyWith<$Res>? get accessControl {
|
||||||
|
if (_value.accessControl == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $AccessControlCopyWith<$Res>(_value.accessControl!, (value) {
|
||||||
|
return _then(_value.copyWith(accessControl: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$PropsImplCopyWith<$Res> implements $PropsCopyWith<$Res> {
|
||||||
|
factory _$$PropsImplCopyWith(
|
||||||
|
_$PropsImpl value, $Res Function(_$PropsImpl) then) =
|
||||||
|
__$$PropsImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({AccessControl? accessControl, bool? allowBypass});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$AccessControlCopyWith<$Res>? get accessControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$PropsImplCopyWithImpl<$Res>
|
||||||
|
extends _$PropsCopyWithImpl<$Res, _$PropsImpl>
|
||||||
|
implements _$$PropsImplCopyWith<$Res> {
|
||||||
|
__$$PropsImplCopyWithImpl(
|
||||||
|
_$PropsImpl _value, $Res Function(_$PropsImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? accessControl = freezed,
|
||||||
|
Object? allowBypass = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$PropsImpl(
|
||||||
|
accessControl: freezed == accessControl
|
||||||
|
? _value.accessControl
|
||||||
|
: accessControl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AccessControl?,
|
||||||
|
allowBypass: freezed == allowBypass
|
||||||
|
? _value.allowBypass
|
||||||
|
: allowBypass // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$PropsImpl implements _Props {
|
||||||
|
const _$PropsImpl({this.accessControl, this.allowBypass});
|
||||||
|
|
||||||
|
factory _$PropsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$PropsImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final AccessControl? accessControl;
|
||||||
|
@override
|
||||||
|
final bool? allowBypass;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Props(accessControl: $accessControl, allowBypass: $allowBypass)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$PropsImpl &&
|
||||||
|
(identical(other.accessControl, accessControl) ||
|
||||||
|
other.accessControl == accessControl) &&
|
||||||
|
(identical(other.allowBypass, allowBypass) ||
|
||||||
|
other.allowBypass == allowBypass));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, accessControl, allowBypass);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
|
||||||
|
__$$PropsImplCopyWithImpl<_$PropsImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$PropsImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _Props implements Props {
|
||||||
|
const factory _Props(
|
||||||
|
{final AccessControl? accessControl,
|
||||||
|
final bool? allowBypass}) = _$PropsImpl;
|
||||||
|
|
||||||
|
factory _Props.fromJson(Map<String, dynamic> json) = _$PropsImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AccessControl? get accessControl;
|
||||||
|
@override
|
||||||
|
bool? get allowBypass;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
|
|||||||
: DAV.fromJson(json['dav'] as Map<String, dynamic>)
|
: DAV.fromJson(json['dav'] as Map<String, dynamic>)
|
||||||
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
|
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
|
||||||
..isCompatible = json['isCompatible'] as bool? ?? true
|
..isCompatible = json['isCompatible'] as bool? ?? true
|
||||||
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true;
|
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
|
||||||
|
..allowBypass = json['allowBypass'] as bool? ?? true;
|
||||||
|
|
||||||
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
|
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
|
||||||
'profiles': instance.profiles,
|
'profiles': instance.profiles,
|
||||||
@@ -52,6 +53,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
|
|||||||
'isAnimateToPage': instance.isAnimateToPage,
|
'isAnimateToPage': instance.isAnimateToPage,
|
||||||
'isCompatible': instance.isCompatible,
|
'isCompatible': instance.isCompatible,
|
||||||
'autoCheckUpdate': instance.autoCheckUpdate,
|
'autoCheckUpdate': instance.autoCheckUpdate,
|
||||||
|
'allowBypass': instance.allowBypass,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$ThemeModeEnumMap = {
|
const _$ThemeModeEnumMap = {
|
||||||
@@ -93,3 +95,17 @@ const _$AccessControlModeEnumMap = {
|
|||||||
AccessControlMode.acceptSelected: 'acceptSelected',
|
AccessControlMode.acceptSelected: 'acceptSelected',
|
||||||
AccessControlMode.rejectSelected: 'rejectSelected',
|
AccessControlMode.rejectSelected: 'rejectSelected',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$PropsImpl _$$PropsImplFromJson(Map<String, dynamic> json) => _$PropsImpl(
|
||||||
|
accessControl: json['accessControl'] == null
|
||||||
|
? null
|
||||||
|
: AccessControl.fromJson(
|
||||||
|
json['accessControl'] as Map<String, dynamic>),
|
||||||
|
allowBypass: json['allowBypass'] as bool?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$PropsImplToJson(_$PropsImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'accessControl': instance.accessControl,
|
||||||
|
'allowBypass': instance.allowBypass,
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const _$GroupTypeEnumMap = {
|
|||||||
GroupType.Selector: 'Selector',
|
GroupType.Selector: 'Selector',
|
||||||
GroupType.URLTest: 'URLTest',
|
GroupType.URLTest: 'URLTest',
|
||||||
GroupType.Fallback: 'Fallback',
|
GroupType.Fallback: 'Fallback',
|
||||||
|
GroupType.LoadBalance: 'LoadBalance',
|
||||||
|
GroupType.Relay: 'Relay',
|
||||||
};
|
};
|
||||||
|
|
||||||
_$ProxyImpl _$$ProxyImplFromJson(Map<String, dynamic> json) => _$ProxyImpl(
|
_$ProxyImpl _$$ProxyImplFromJson(Map<String, dynamic> json) => _$ProxyImpl(
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ class _ScanPageState extends State<ScanPage> with WidgetsBindingObserver {
|
|||||||
case TorchState.unavailable:
|
case TorchState.unavailable:
|
||||||
icon = const Icon(Icons.flash_off);
|
icon = const Icon(Icons.flash_off);
|
||||||
backgroundColor = Colors.transparent;
|
backgroundColor = Colors.transparent;
|
||||||
|
case TorchState.auto:
|
||||||
|
icon = const Icon(Icons.flash_auto);
|
||||||
|
backgroundColor = Colors.orange;
|
||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
|||||||
@@ -61,8 +61,14 @@ class GlobalState {
|
|||||||
required Config config,
|
required Config config,
|
||||||
required ClashConfig clashConfig,
|
required ClashConfig clashConfig,
|
||||||
}) async {
|
}) async {
|
||||||
final args =
|
final args = config.isAccessControl
|
||||||
config.isAccessControl ? json.encode(config.accessControl) : null;
|
? json.encode(
|
||||||
|
Props(
|
||||||
|
accessControl: config.accessControl,
|
||||||
|
allowBypass: config.allowBypass,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
await proxyManager.startProxy(
|
await proxyManager.startProxy(
|
||||||
port: clashConfig.mixedPort,
|
port: clashConfig.mixedPort,
|
||||||
args: args,
|
args: args,
|
||||||
@@ -105,12 +111,6 @@ class GlobalState {
|
|||||||
clashConfig: clashConfig,
|
clashConfig: clashConfig,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!appState.isInit) return;
|
|
||||||
await applyProfile(
|
|
||||||
appState: appState,
|
|
||||||
config: config,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
);
|
|
||||||
updateCoreVersionInfo(appState);
|
updateCoreVersionInfo(appState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,19 +133,6 @@ class GlobalState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNavigationItems({
|
|
||||||
required AppState appState,
|
|
||||||
required Config config,
|
|
||||||
required ClashConfig clashConfig,
|
|
||||||
}) {
|
|
||||||
final group = appState.currentGroups;
|
|
||||||
final hasProfile = config.profiles.isNotEmpty;
|
|
||||||
appState.navigationItems = navigation.getItems(
|
|
||||||
openLogs: config.openLogs,
|
|
||||||
hasProxies: group.isNotEmpty && hasProfile,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateGroups(AppState appState) async {
|
Future<void> updateGroups(AppState appState) async {
|
||||||
appState.groups = await clashCore.getProxiesGroups();
|
appState.groups = await clashCore.getProxiesGroups();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
|||||||
return floatingActionButton ?? Container();
|
return floatingActionButton ?? Container();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
|
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
|||||||
304
pubspec.lock
@@ -1,7 +1,7 @@
|
|||||||
name: fl_clash
|
name: fl_clash
|
||||||
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.8.16
|
version: 0.8.20
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.1.0 <4.0.0'
|
sdk: '>=3.1.0 <4.0.0'
|
||||||
|
|
||||||
@@ -17,14 +17,14 @@ dependencies:
|
|||||||
provider: ^6.0.5
|
provider: ^6.0.5
|
||||||
window_manager: ^0.3.8
|
window_manager: ^0.3.8
|
||||||
ffi: ^2.1.0
|
ffi: ^2.1.0
|
||||||
dynamic_color: ^1.7.0
|
dynamic_color: ^1.6.0
|
||||||
proxy:
|
proxy:
|
||||||
path: plugins/proxy
|
path: plugins/proxy
|
||||||
launch_at_startup: ^0.2.2
|
launch_at_startup: ^0.2.2
|
||||||
windows_single_instance: ^1.0.1
|
windows_single_instance: ^1.0.1
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
file_picker: ^8.0.3
|
file_picker: ^8.0.3
|
||||||
mobile_scanner: 5.0.1
|
mobile_scanner: ^5.1.1
|
||||||
app_links: ^3.5.0
|
app_links: ^3.5.0
|
||||||
win32_registry: ^1.1.2
|
win32_registry: ^1.1.2
|
||||||
tray_manager: ^0.2.1
|
tray_manager: ^0.2.1
|
||||||
|
|||||||
17
setup.dart
@@ -12,10 +12,7 @@ enum PlatformType {
|
|||||||
macos,
|
macos,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Arch {
|
enum Arch { amd64, arm64, arm }
|
||||||
amd64,
|
|
||||||
arm64,
|
|
||||||
}
|
|
||||||
|
|
||||||
class BuildLibItem {
|
class BuildLibItem {
|
||||||
PlatformType platform;
|
PlatformType platform;
|
||||||
@@ -64,6 +61,11 @@ class Build {
|
|||||||
arch: Arch.amd64,
|
arch: Arch.amd64,
|
||||||
archName: 'amd64',
|
archName: 'amd64',
|
||||||
),
|
),
|
||||||
|
BuildLibItem(
|
||||||
|
platform: PlatformType.android,
|
||||||
|
arch: Arch.arm,
|
||||||
|
archName: 'armeabi-v7a',
|
||||||
|
),
|
||||||
BuildLibItem(
|
BuildLibItem(
|
||||||
platform: PlatformType.android,
|
platform: PlatformType.android,
|
||||||
arch: Arch.arm64,
|
arch: Arch.arm64,
|
||||||
@@ -334,7 +336,7 @@ class BuildCommand extends Command {
|
|||||||
final archName = argResults?['arch'];
|
final archName = argResults?['arch'];
|
||||||
final currentArches =
|
final currentArches =
|
||||||
arches.where((element) => element.name == archName).toList();
|
arches.where((element) => element.name == archName).toList();
|
||||||
final arch = currentArches.isEmpty ? null : arches.first;
|
final arch = currentArches.isEmpty ? null : currentArches.first;
|
||||||
await _buildLib(arch);
|
await _buildLib(arch);
|
||||||
if (build != "all") {
|
if (build != "all") {
|
||||||
return;
|
return;
|
||||||
@@ -357,10 +359,11 @@ class BuildCommand extends Command {
|
|||||||
break;
|
break;
|
||||||
case PlatformType.android:
|
case PlatformType.android:
|
||||||
final targetMap = {
|
final targetMap = {
|
||||||
|
Arch.arm: "android-arm",
|
||||||
|
Arch.arm64: "android-arm64",
|
||||||
Arch.amd64: "android-x64",
|
Arch.amd64: "android-x64",
|
||||||
Arch.arm64: "android-arm64"
|
|
||||||
};
|
};
|
||||||
final defaultArches = [Arch.amd64, Arch.arm64];
|
final defaultArches = [Arch.arm, Arch.arm64, Arch.amd64];
|
||||||
final defaultTargets = defaultArches
|
final defaultTargets = defaultArches
|
||||||
.where((element) => arch == null ? true : element == arch)
|
.where((element) => arch == null ? true : element == arch)
|
||||||
.map((e) => targetMap[e])
|
.map((e) => targetMap[e])
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// ignore_for_file: avoid_print
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
String input = """
|
String input = """
|
||||||
您
|
您
|
||||||
|
|||||||