Compare commits

..

22 Commits

Author SHA1 Message Date
chen08209
86572cc960 Fix ipv6 error 2024-06-12 19:07:54 +08:00
chen08209
ee22709d49 Fix android udp direct error
Add ipv6 switch

Add access all selected button

Remove android low version splash
2024-06-12 18:29:58 +08:00
chen08209
0a2ad63f38 Update version 2024-06-10 19:11:04 +08:00
chen08209
2ec12c9363 Add allowBypass
Fix Android only pick .text file issues
2024-06-10 19:09:58 +08:00
chen08209
a3c2dc786c Fix search issues 2024-06-09 21:48:17 +08:00
chen08209
7acf9c6db3 Fix LoadBalance, Relay load error 2024-06-09 20:53:36 +08:00
chen08209
8074547fb4 Fix build.yml4 2024-06-09 19:56:51 +08:00
chen08209
8a01e04871 Fix build.yml3 2024-06-09 19:49:51 +08:00
chen08209
7ddcdd9828 Fix build.yml2 2024-06-09 19:49:14 +08:00
chen08209
d89ed076fd Fix build.yml 2024-06-09 19:46:05 +08:00
chen08209
f4c3b06cd5 Add search function at access control
Fix the issues with the profile add button to cover the edit button

Adapt LoadBalance and Relay

Add arm

Fix android notification icon error
2024-06-09 19:25:14 +08:00
chen08209
c65746709d Add one-click update all profiles
Add expire show
2024-06-08 15:43:28 +08:00
chen08209
d2d9bdab02 Temp remove tun mode 2024-06-06 22:58:32 +08:00
chen08209
d0f8444b6d Remove macos in workflow 2024-06-06 22:29:26 +08:00
chen08209
fccabfbe27 Change go version 2024-06-06 22:04:29 +08:00
chen08209
e9bb97c6ce Update Version
Fix tun unable to open
2024-06-06 17:36:49 +08:00
chen08209
068fe14d89 Optimize delay test2 2024-06-06 17:13:32 +08:00
chen08209
43c397007c Optimize delay test
Add check ip
2024-06-06 16:35:09 +08:00
chen08209
5e801d5f5e add check ip request 2024-06-06 16:34:31 +08:00
chen08209
52d61b15fd Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the application to flash back.
Fix edit profile error
2024-06-06 10:15:51 +08:00
chen08209
fea3c14608 Fix quickStart change proxy error 2024-06-05 17:59:53 +08:00
chen08209
84be01a38a Fix core version 2024-06-05 17:59:50 +08:00
69 changed files with 2004 additions and 1115 deletions

View File

@@ -21,6 +21,12 @@ jobs:
os: macos-13
steps:
- name: Check Matrix
run: |
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
- name: Checkout
uses: actions/checkout@v4
with:
@@ -82,6 +88,7 @@ jobs:
upload-release:
if: ${{ !contains(github.ref, '+') }}
permissions: write-all
needs: [ build ]
runs-on: ubuntu-latest

2
.gitignore vendored
View File

@@ -31,6 +31,7 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
/dist/
# Symbolication related
app.*.symbols
@@ -44,6 +45,7 @@ app.*.map.json
/android/app/release
#libclash
/libclash/

2
.gitmodules vendored
View File

@@ -5,4 +5,4 @@
[submodule "plugins/flutter_distributor"]
path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git
branch = main
branch = FlClash

View File

@@ -62,7 +62,7 @@ android {
defaultConfig {
applicationId "com.follow.clash"
minSdkVersion 21
minSdkVersion 24
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@@ -1,9 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
@@ -17,12 +14,15 @@
<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.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:extractNativeLibs="true"
android:networkSecurityConfig="@xml/network_security_config"
android:label="FlClash">
<activity
@@ -73,7 +73,8 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:icon="@drawable/tile_icon"
android:icon="@drawable/icon"
android:foregroundServiceType="specialUse"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>

View File

@@ -10,3 +10,8 @@ data class AccessControl(
val acceptList: List<String>,
val rejectList: List<String>,
)
data class Props (
val accessControl: AccessControl?,
val allowBypass: Boolean?,
)

View File

@@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.AccessControl
import com.follow.clash.models.Props
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
@@ -41,7 +42,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
private var flClashVpnService: FlClashVpnService? = null
private var isBound = false
private var port: Int? = null
private var accessControl: AccessControl? = null
private var props: Props? = null
private lateinit var title: String
private lateinit var content: String
@@ -73,8 +74,8 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
"StartProxy" -> {
port = call.argument<Int>("port")
val args = call.argument<String>("args")
accessControl =
if (args != null) Gson().fromJson(args, AccessControl::class.java) else null
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
handleStartVpn()
result.success(true)
}
@@ -121,7 +122,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
private fun startVpn(port: Int) {
if (GlobalState.runState.value == RunState.START) return;
flClashVpnService?.start(port, accessControl)
flClashVpnService?.start(port, props)
GlobalState.runState.value = RunState.START
GlobalState.runTime = Date()
startAfter()

View File

@@ -19,7 +19,6 @@ import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
@RequiresApi(Build.VERSION_CODES.N)
class FlClashTileService : TileService() {
private val observer = Observer<RunState> { runState ->
@@ -43,19 +42,27 @@ class FlClashTileService : TileService() {
GlobalState.runState.observeForever(observer)
}
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun activityTransfer() {
val intent = Intent(this, TempActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (Build.VERSION.SDK_INT >= 34) {
val pendingIntent = PendingIntent.getActivity(
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
startActivityAndCollapse(pendingIntent)
} 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)
}
}

View File

@@ -1,10 +1,9 @@
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.net.ProxyInfo
@@ -13,15 +12,14 @@ import android.os.Binder
import android.os.Build
import android.os.IBinder
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.MainActivity
import com.follow.clash.R
import com.follow.clash.models.AccessControl
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.Props
@SuppressLint("WrongConstant")
class FlClashVpnService : VpnService() {
@@ -54,12 +52,12 @@ class FlClashVpnService : VpnService() {
return START_STICKY
}
fun start(port: Int, accessControl: AccessControl?) {
fun start(port: Int, props: Props?) {
fd = with(Builder()) {
addAddress("172.16.0.1", 30)
setMtu(9000)
addRoute("0.0.0.0", 0)
if (accessControl != null) {
props?.accessControl?.let { accessControl ->
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
@@ -80,7 +78,9 @@ class FlClashVpnService : VpnService() {
if (Build.VERSION.SDK_INT >= 29) {
setMetered(false)
}
allowBypass()
if (props?.allowBypass == true) {
allowBypass()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setHttpProxy(
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 pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
@@ -119,34 +119,34 @@ class FlClashVpnService : VpnService() {
)
}
val icon = IconCompat.createWithResource(this, this.applicationInfo.icon)
with(NotificationCompat.Builder(this, CHANNEL)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setSmallIcon(icon)
}
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
setContentIntent(pendingIntent)
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)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true);
}
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
channel.setShowBadge(false)
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
@@ -155,7 +155,7 @@ class FlClashVpnService : VpnService() {
private fun stopForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(Service.STOP_FOREGROUND_REMOVE)
stopForeground(STOP_FOREGROUND_REMOVE)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -4,7 +4,6 @@
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@mipmap/ic_launcher_foreground</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -4,7 +4,6 @@
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@mipmap/ic_launcher_foreground</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -321,20 +321,23 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig, compatible bool) {
targetConfig.ExternalController = ""
targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = ""
targetConfig.Interface = ""
targetConfig.ExternalUIURL = ""
//targetConfig.IPv6 = patchConfig.IPv6
targetConfig.GeodataMode = false
targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
targetConfig.SocksPort = 0
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.FindProcessMode = process.FindProcessAlways
targetConfig.AllowLan = patchConfig.AllowLan
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.Mode = patchConfig.Mode
targetConfig.Tun.Enable = patchConfig.Tun.Enable
targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack
//targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
//targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.GeodataLoader = "standard"
targetConfig.Profile.StoreSelected = false
if targetConfig.DNS.Enable == false {
@@ -350,7 +353,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.RuleProvider = make(map[string]map[string]any)
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
}
}
func patchConfig(general *config.General) {
@@ -409,6 +411,6 @@ func applyConfig(isPatch bool) {
patchConfig(cfg.General)
} else {
executor.ApplyConfig(cfg, true)
healthcheck()
hcCompatibleProvider(tunnel.Providers())
}
}

View File

@@ -35,19 +35,16 @@ github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIF
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -63,7 +60,6 @@ github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -73,7 +69,6 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -88,9 +83,7 @@ github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6K
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -132,7 +125,6 @@ github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:U
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
@@ -154,7 +146,6 @@ github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
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/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
@@ -258,7 +249,6 @@ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
@@ -273,7 +263,6 @@ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFW
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -65,12 +65,7 @@ func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
go func() {
bytes := []byte(C.GoString(s))
rawConfig, err := config.UnmarshalRawConfig(bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
_, err = config.ParseRawConfig(rawConfig)
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
@@ -186,7 +181,8 @@ func getTraffic() *C.char {
}
//export asyncTestDelay
func asyncTestDelay(s *C.char) {
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
go func() {
paramsString := C.GoString(s)
var params = &TestDelayParams{}
@@ -210,26 +206,25 @@ func asyncTestDelay(s *C.char) {
Name: params.ProxyName,
}
message := bridge.Message{
Type: bridge.Delay,
Data: delayData,
}
if proxy == nil {
delayData.Value = -1
bridge.SendMessage(message)
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return
}
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
if err != nil || delay == 0 {
delayData.Value = -1
bridge.SendMessage(message)
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return
}
delayData.Value = int32(delay)
bridge.SendMessage(message)
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return
}()
}
@@ -237,7 +232,7 @@ func asyncTestDelay(s *C.char) {
func getVersionInfo() *C.char {
versionInfo := map[string]string{
"clashName": constant.Name,
"version": "v1.18.5",
"version": "1.18.5",
}
data, err := json.Marshal(versionInfo)
if err != nil {
@@ -379,11 +374,6 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l
}()
}
//export healthcheck
func healthcheck() {
hcCompatibleProvider(tunnel.Providers())
}
//export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer, port C.longlong) {
bridge.InitDartApi(api)

View File

@@ -17,36 +17,35 @@ var tunLock sync.Mutex
var tun *t.Tun
//export startTUN
func startTUN(fd C.int) bool {
tunLock.Lock()
defer tunLock.Unlock()
func startTUN(fd C.int) {
go func() {
tunLock.Lock()
defer tunLock.Unlock()
if tun != nil {
tun.Close()
tun = nil
}
f := int(fd)
gateway := "172.16.0.1/30"
portal := "172.16.0.2"
dns := "0.0.0.0"
if tun != nil {
tun.Close()
tun = nil
}
f := int(fd)
gateway := "172.16.0.1/30"
portal := "172.16.0.2"
dns := "0.0.0.0"
tempTun := &t.Tun{Closed: false, Limit: semaphore.NewWeighted(4)}
tempTun := &t.Tun{Closed: false, Limit: semaphore.NewWeighted(4)}
closer, err := t.Start(f, gateway, portal, dns)
closer, err := t.Start(f, gateway, portal, dns)
applyConfig(true)
applyConfig(true)
if err != nil {
log.Errorln("startTUN error: %v", err)
tempTun.Close()
return false
}
if err != nil {
log.Errorln("startTUN error: %v", err)
tempTun.Close()
}
tempTun.Closer = closer
tempTun.Closer = closer
tun = tempTun
return true
tun = tempTun
}()
}
//export updateMarkSocketPort
@@ -61,14 +60,16 @@ func updateMarkSocketPort(markSocketPort C.longlong) bool {
//export stopTun
func stopTun() {
tunLock.Lock()
defer tunLock.Unlock()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
if tun != nil {
tun.Close()
applyConfig(true)
tun = nil
}
if tun != nil {
tun.Close()
applyConfig(true)
tun = nil
}
}()
}
func init() {

View File

@@ -82,7 +82,7 @@ class ApplicationState extends State<Application> {
super.initState();
globalState.appController = AppController(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
globalState.appController.afterInit();
await globalState.appController.init();
globalState.appController.initLink();
_updateGroups();
});
@@ -160,8 +160,9 @@ class ApplicationState extends State<Application> {
AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
pageTransitionsTheme: _pageTransitionsTheme,
useMaterial3: true,
fontFamily: '',
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
@@ -170,6 +171,7 @@ class ApplicationState extends State<Application> {
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: '',
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,

View File

@@ -156,23 +156,36 @@ class ClashCore {
return clashFFI.changeProxy(params.toNativeUtf8().cast()) == 1;
}
bool delay(String proxyName) {
Future<Delay> getDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
clashFFI.asyncTestDelay(json.encode(delayParams).toNativeUtf8().cast());
return true;
final completer = Completer<Delay>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(Delay.fromJson(json.decode(message)));
receiver.close();
}
});
clashFFI.asyncTestDelay(
json.encode(delayParams).toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
Future.delayed(httpTimeoutDuration + moreDuration, () {
receiver.close();
completer.complete(
Delay(name: proxyName, value: -1),
);
});
return completer.future;
}
clearEffect(String path) {
clashFFI.clearEffect(path.toNativeUtf8().cast());
}
healthcheck() {
clashFFI.healthcheck();
}
VersionInfo getVersionInfo() {
final versionInfoRaw = clashFFI.getVersionInfo();
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());

View File

@@ -977,17 +977,20 @@ class ClashFFI {
void asyncTestDelay(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _asyncTestDelay(
s,
port,
);
}
late final _asyncTestDelayPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'asyncTestDelay');
late final _asyncTestDelay =
_asyncTestDelayPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
late final _asyncTestDelayPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('asyncTestDelay');
late final _asyncTestDelay = _asyncTestDelayPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getVersionInfo() {
return _getVersionInfo();
@@ -1086,14 +1089,6 @@ class ClashFFI {
late final _updateExternalProvider = _updateExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void healthcheck() {
return _healthcheck();
}
late final _healthcheckPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('healthcheck');
late final _healthcheck = _healthcheckPtr.asFunction<void Function()>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
int port,

View File

@@ -18,6 +18,7 @@ const configKey = "config";
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
const double dialogCommonWidth = 300;
const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600;
const maxLaptopWidth = 840;
final filter = ImageFilter.blur(

View File

@@ -35,4 +35,8 @@ extension DateTimeExtension on DateTime {
}
return appLocalizations.just;
}
}
String get show {
return toIso8601String().substring(0, 10);
}
}

View File

@@ -23,10 +23,11 @@ class Measure {
double? _bodyMediumHeight;
double? _bodySmallHeight;
double? _labelSmallHeight;
double? _labelMediumHeight;
double? _titleLargeHeight;
double? _titleMediumHeight;
double get bodyMediumHeight{
double get bodyMediumHeight {
_bodyMediumHeight ??= computeTextSize(
Text(
"",
@@ -36,7 +37,7 @@ class Measure {
return _bodyMediumHeight!;
}
double get bodySmallHeight{
double get bodySmallHeight {
_bodySmallHeight ??= computeTextSize(
Text(
"",
@@ -46,7 +47,7 @@ class Measure {
return _bodySmallHeight!;
}
double get labelSmallHeight{
double get labelSmallHeight {
_labelSmallHeight ??= computeTextSize(
Text(
"",
@@ -56,7 +57,17 @@ class Measure {
return _labelSmallHeight!;
}
double get titleLargeHeight{
double get labelMediumHeight {
_labelMediumHeight ??= computeTextSize(
Text(
"",
style: context.textTheme.labelMedium,
),
).height;
return _labelMediumHeight!;
}
double get titleLargeHeight {
_titleLargeHeight ??= computeTextSize(
Text(
"",
@@ -65,4 +76,14 @@ class Measure {
).height;
return _titleLargeHeight!;
}
double get titleMediumHeight {
_titleMediumHeight ??= computeTextSize(
Text(
"",
style: context.textTheme.titleMedium,
),
).height;
return _titleMediumHeight!;
}
}

View File

@@ -10,8 +10,7 @@ class Picker {
if (Platform.isAndroid) {
filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
type: FileType.custom,
allowedExtensions: ['txt', 'conf'],
allowMultiple: false,
);
} else {
filePickerResult = await FilePicker.platform.pickFiles(

View File

@@ -3,18 +3,17 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/ip.dart';
import 'package:fl_clash/state.dart';
class Request {
late final Dio _dio;
int? _port;
bool _isStart = false;
Request() {
_dio = Dio(
BaseOptions(
connectTimeout: httpTimeoutDuration,
sendTimeout: httpTimeoutDuration,
receiveTimeout: httpTimeoutDuration,
headers: {"User-Agent": coreName},
),
);
@@ -26,13 +25,16 @@ class Request {
));
}
_syncProxy(){
_syncProxy() {
final port = globalState.appController.clashConfig.mixedPort;
if (_port != port) {
final isStart = globalState.appController.appState.isStart;
if (_port != port || isStart != _isStart) {
_port = port;
_isStart = isStart;
_dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
if (!_isStart) return client;
client.findProxy = (url) {
return "PROXY localhost:$_port;DIRECT";
};
@@ -45,14 +47,14 @@ class Request {
Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio
.get(
url,
options: Options(
responseType: ResponseType.bytes,
),
)
url,
options: Options(
responseType: ResponseType.bytes,
),
)
.timeout(
httpTimeoutDuration,
);
httpTimeoutDuration * 2,
);
return response;
}
@@ -73,6 +75,31 @@ class Request {
if (!hasUpdate) return null;
return data;
}
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<IpInfo?> checkIp(CancelToken? cancelToken) async {
for (final source in _ipInfoSources.entries) {
try {
final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.timeout(
httpTimeoutDuration,
);
if (response.statusCode == 200 && response.data != null) {
return source.value(response.data!);
}
} catch (e) {
continue;
}
}
return null;
}
}
final request = Request();

View File

@@ -6,6 +6,11 @@ extension TextStyleExtension on TextStyle {
return copyWith(color: color?.toLight());
}
toLighter() {
return copyWith(color: color?.toLighter());
}
toSoftBold() {
return copyWith(fontWeight: FontWeight.w500);
}

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -39,8 +40,6 @@ class AppController {
updateRunTime,
updateTraffic,
];
clearShowProxyDelay();
testShowProxyDelay();
} else {
await globalState.stopSystemProxy();
appState.traffics = [];
@@ -98,10 +97,12 @@ class AppController {
}
}
updateProfile(String id) async {
Future<void> updateProfile(String id) async {
final profile = config.getCurrentProfileForId(id);
if (profile != null) {
await profile.update();
final tempProfile = profile.copyWith();
await tempProfile.update();
config.setProfile(tempProfile);
}
}
@@ -135,16 +136,25 @@ class AppController {
autoUpdateProfiles() async {
for (final profile in config.profiles) {
if (!profile.autoUpdate) return;
if (!profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate
?.add(
profile.autoUpdateDuration,
)
.isBeforeNow;
if (isNotNeedUpdate == false ||
profile.url == null ||
profile.url!.isEmpty) continue;
await profile.update();
if (isNotNeedUpdate == false || profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile.id);
}
}
updateProfiles() async {
for (final profile in config.profiles) {
if (profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile.id);
}
}
@@ -246,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 {
if (config.autoRun) {
await updateSystemProxy(true);
@@ -255,25 +288,9 @@ class AppController {
}
autoUpdateProfiles();
updateLogStatus();
if (!config.silentLaunch) {
window?.show();
}
autoCheckUpdate();
}
healthcheck() {
if (globalState.healthcheckLock) return;
for (final delay in appState.delayMap.entries) {
setDelay(
Delay(
name: delay.key,
value: 0,
),
);
}
clashCore.healthcheck();
}
setDelay(Delay delay) {
appState.setDelay(delay);
}
@@ -383,22 +400,6 @@ class AppController {
addProfileFormURL(url);
}
clearShowProxyDelay() {
final showProxyDelay = appState.getRealProxyName(appState.showProxyName);
if (showProxyDelay != null) {
appState.setDelay(
Delay(name: showProxyDelay, value: null),
);
}
}
testShowProxyDelay() {
final showProxyDelay = appState.getRealProxyName(appState.showProxyName);
if (showProxyDelay != null) {
globalState.updateCurrentDelay(showProxyDelay);
}
}
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;

View File

@@ -1,6 +1,6 @@
// ignore_for_file: constant_identifier_names
enum GroupType { Selector, URLTest, Fallback }
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
enum GroupName { GLOBAL, Proxy, Auto, Fallback }

View File

@@ -8,6 +8,13 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.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});
@@ -83,136 +90,70 @@ class _AccessFragmentState extends State<AccessFragment> {
);
}
Widget _buildSelectedAllButton({
required bool isSelectedAll,
required List<String> allValueList,
}) {
return Builder(
builder: (context) {
final tooltip = isSelectedAll
? appLocalizations.cancelSelectAll
: appLocalizations.selectAll;
return IconButton(
tooltip: tooltip,
onPressed: () {
final config = globalState.appController.config;
final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: [],
),
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
icon: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
);
Widget _buildSearchButton(List<Package> packages) {
return IconButton(
tooltip: appLocalizations.search,
onPressed: () {
showSearch(
context: context,
delegate: AccessControlSearchDelegate(
packages: packages,
),
).then((_) => {setState(() {})});
},
icon: const Icon(Icons.search),
);
}
Widget _actionHeader({
_buildSelectedAllButton({
required bool isAccessControl,
required List<String> valueList,
required String describe,
required List<String> packageNameList,
required bool isSelectedAll,
required List<String> allValueList,
}) {
return AbsorbPointer(
absorbing: !isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color:
Theme.of(context).colorScheme.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color:
Theme.of(context).colorScheme.primary,
),
),
),
],
),
WidgetsBinding.instance.addPostFrameCallback((_) {
final tooltip = isSelectedAll
? appLocalizations.cancelSelectAll
: appLocalizations.selectAll;
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DisabledMask(
status: !isAccessControl,
child: 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: [],
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSelectedAllButton(
isSelectedAll: const ListEquality<String>()
.equals(valueList, packageNameList),
allValueList: packageNameList,
),
),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
child: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
),
),
),
);
);
});
}
Widget _buildPackageList() {
@@ -252,14 +193,11 @@ class _AccessFragmentState extends State<AccessFragment> {
accessControlMode == AccessControlMode.acceptSelected
? acceptPackages
: rejectPackages;
final currentList =
accessControlMode == AccessControlMode.acceptSelected
? accessControl.acceptList
: accessControl.rejectList;
final currentList = accessControl.currentList;
final currentPackages = isFilterSystemApp
? packages
.where((element) => element.isSystem == false)
.toList()
.where((element) => element.isSystem == false)
.toList()
: packages;
final packageNameList =
currentPackages.map((e) => e.packageName).toList();
@@ -268,15 +206,91 @@ class _AccessFragmentState extends State<AccessFragment> {
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
_buildSelectedAllButton(
isAccessControl: isAccessControl,
isSelectedAll: valueList.length == packageNameList.length,
allValueList: packageNameList,
);
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
_actionHeader(
isAccessControl: isAccessControl,
valueList: valueList,
describe: describe,
packageNameList: packageNameList,
AbsorbPointer(
absorbing: !isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(currentPackages)),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
),
),
),
Expanded(
flex: 1,
@@ -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);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
@@ -54,8 +56,8 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onTab: () {
_modifyMixedPort(mixedPort);
},
padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 4),
leading: const Icon(Icons.adjust),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
trailing: FilledButton.tonal(
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) => clashConfig.allowLan,
builder: (_, allowLan, __) {
@@ -86,20 +106,38 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
if (system.isDesktop)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
// if (system.isDesktop)
// Selector<ClashConfig, bool>(
// selector: (_, clashConfig) => clashConfig.tun.enable,
// builder: (_, tunEnable, __) {
// return ListItem.switchItem(
// leading: const Icon(Icons.support),
// title: Text(appLocalizations.tun),
// subtitle: Text(appLocalizations.tunDesc),
// delegate: SwitchDelegate(
// value: tunEnable,
// onChanged: (bool value) async {
// final clashConfig = context.read<ClashConfig>();
// clashConfig.tun = Tun(enable: value);
// globalState.appController.updateClashConfigDebounce();
// },
// ),
// );
// },
// ),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.support),
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
leading: const Icon(Icons.double_arrow),
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: tunEnable,
value: allowBypass,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value);
globalState.appController.updateClashConfigDebounce();
final appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
@@ -109,7 +147,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
return ListItem.switchItem(
leading: const Icon(Icons.expand),
leading: const Icon(Icons.expand_outlined),
title: Text(appLocalizations.compatible),
subtitle: Text(appLocalizations.compatibleDesc),
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: kMaterialListPadding,
child: Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.feedback),
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
trailing: SizedBox(
height: 48,
child: DropdownMenu<LogLevel>(
width: 124,
inputDecorationTheme: const InputDecorationTheme(
filled: true,
contentPadding: EdgeInsets.symmetric(
vertical: 5,
horizontal: 16,
),
),
initialSelection: value,
dropdownMenuEntries: [
for (final logLevel in LogLevel.values)

View File

@@ -1,3 +1,5 @@
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';
import 'package:fl_clash/state.dart';
@@ -13,118 +15,162 @@ class NetworkDetection extends StatefulWidget {
}
class _NetworkDetectionState extends State<NetworkDetection> {
Widget _buildDescription(String? currentProxyName, int? delay) {
if (currentProxyName == null) {
return TooltipText(
text: Text(
appLocalizations.noProxyDesc,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
overflow: TextOverflow.ellipsis,
),
);
final ipInfoNotifier = ValueNotifier<IpInfo?>(null);
final timeoutNotifier = ValueNotifier<bool>(false);
bool? _preIsStart;
CancelToken? cancelToken;
_checkIp(
bool isInit,
bool isStart,
) async {
if (!isInit) return;
timeoutNotifier.value = false;
if (_preIsStart == false && _preIsStart == isStart) return;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
if (delay == 0 || delay == null) {
return const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(
strokeCap: StrokeCap.round,
),
);
ipInfoNotifier.value = null;
final ipInfo = await request.checkIp(cancelToken);
if (ipInfo == null) {
timeoutNotifier.value = true;
return;
} else {
timeoutNotifier.value = false;
}
if (delay > 0) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TooltipText(
text: Text(
"$delay",
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: context.textTheme.titleLarge
?.copyWith(
color: context.colorScheme.primary,
)
.toSoftBold(),
),
),
const Flexible(
child: SizedBox(
width: 4,
),
),
Flexible(
flex: 0,
child: Text(
'ms',
style: Theme.of(context).textTheme.bodyMedium?.toLight(),
),
),
],
);
}
return Text(
"Timeout",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.red,
),
_preIsStart = isStart;
ipInfoNotifier.value = ipInfo;
}
_checkIpContainer(Widget child) {
return Selector2<AppState, Config, CheckIpSelectorState>(
selector: (_, appState, config) {
return CheckIpSelectorState(
isInit: appState.isInit,
selectedMap: appState.selectedMap,
isStart: appState.isStart,
);
},
builder: (_, state, __) {
_checkIp(state.isInit, state.isStart);
return child;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return CommonCard(
info: Info(
iconData: Icons.network_check,
label: appLocalizations.networkDetection,
),
child: Selector<AppState, NetworkDetectionSelectorState>(
selector: (_, appState) {
return NetworkDetectionSelectorState(
currentProxyName: appState.showProxyName,
delay: appState.getDelay(
appState.showProxyName,
),
);
},
builder: (_, state, __) {
return Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
return _checkIpContainer(
ValueListenableBuilder<IpInfo?>(
valueListenable: ipInfoNotifier,
builder: (_, ipInfo, __) {
return CommonCard(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 0,
child: TooltipText(
text: Text(
state.currentProxyName ?? appLocalizations.noProxy,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleMedium
?.toSoftBold(),
),
),
),
const SizedBox(
height: 8,
),
Flexible(
child: Container(
height: globalState.appController.measure.titleLargeHeight,
alignment: Alignment.centerLeft,
child: FadeBox(
child: _buildDescription(
state.currentProxyName,
state.delay,
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.network_check,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: FadeBox(
child: ipInfo != null
? CountryFlag.fromCountryCode(
ipInfo.countryCode,
width: 24,
height: 24,
)
: ValueListenableBuilder(
valueListenable: timeoutNotifier,
builder: (_, timeout, __) {
if (timeout) {
return Text(
appLocalizations.checkError,
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,
),
);
},
),
),
),
],
),
),
),
Container(
height:
globalState.appController.measure.titleLargeHeight + 24,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox(
child: ipInfo != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
ipInfo.ip,
style: context.textTheme.titleLarge
?.toSoftBold(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
)
: ValueListenableBuilder(
valueListenable: timeoutNotifier,
builder: (_, timeout, __) {
if (timeout) {
return Text(
"timeout",
style: context.textTheme.titleMedium
?.copyWith(color: Colors.red)
.toSoftBold(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
);
},
),
),
)
],
),
);

View File

@@ -19,7 +19,7 @@ class _StartButtonState extends State<StartButton>
@override
void initState() {
isStart = context.read<AppState>().runTime != null;
isStart = globalState.appController.appState.isStart;
_controller = AnimationController(
vsync: this,
value: isStart ? 1 : 0,
@@ -48,9 +48,11 @@ class _StartButtonState extends State<StartButton>
}
}
updateSystemProxy() async {
final appController = globalState.appController;
await appController.updateSystemProxy(isStart);
updateSystemProxy() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final appController = globalState.appController;
await appController.updateSystemProxy(isStart);
});
}
@override

View File

@@ -56,6 +56,9 @@ class _LogsFragmentState extends State<LogsFragment> {
);
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
)
];
});
@@ -139,6 +142,9 @@ class LogsSearchDelegate extends SearchDelegate {
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}

View File

@@ -1,4 +1,5 @@
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';
@@ -40,7 +41,7 @@ class _EditProfileState extends State<EditProfile> {
_handleConfirm() {
if (!_formKey.currentState!.validate()) return;
final config = widget.context.read<Config>();
final hasUpdate = widget.profile.url != urlController.text;
final hasUpdate = urlController.text.isNotEmpty && widget.profile.url != urlController.text;
widget.profile.url = urlController.text;
widget.profile.label = labelController.text;
widget.profile.autoUpdate = autoUpdate;
@@ -82,7 +83,7 @@ class _EditProfileState extends State<EditProfile> {
},
),
),
if (widget.profile.url != null)...[
if (widget.profile.type == ProfileType.url)...[
ListItem(
title: TextFormField(
controller: urlController,

View File

@@ -25,15 +25,7 @@ class ProfilesFragment extends StatefulWidget {
}
class _ProfilesFragmentState extends State<ProfilesFragment> {
_handleDeleteProfile(String id) async {
globalState.appController.deleteProfile(id);
}
_handleUpdateProfile(String id) async {
context.findAncestorStateOfType<CommonScaffoldState>()?.loadingRun(
() => globalState.appController.updateProfile(id),
);
}
final hasPadding = ValueNotifier<bool>(false);
_handleShowAddExtendPage() {
showExtendPage(
@@ -45,76 +37,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
}
_handleShowEditExtendPage(Profile profile) {
showExtendPage(
context,
body: EditProfile(
profile: profile.copyWith(),
context: context,
),
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
}
_buildGrid({
required ProfilesSelectorState state,
int crossAxisCount = 1,
}) {
return SingleChildScrollView(
padding: crossAxisCount > 1
? const EdgeInsets.symmetric(horizontal: 16)
: EdgeInsets.zero,
child: Grid.baseGap(
crossAxisCount: crossAxisCount,
children: [
for (final profile in state.profiles)
GridItem(
child: ProfileItem(
profile: profile,
commonPopupMenu: CommonPopupMenu<ProfileActions>(
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,
label: appLocalizations.edit,
iconData: Icons.edit,
),
if (profile.url != null)
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(profile);
break;
case ProfileActions.delete:
_handleDeleteProfile(profile.id);
break;
case ProfileActions.update:
_handleUpdateProfile(profile.id);
break;
case null:
break;
}
},
),
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
);
}
_getColumns(ViewMode viewMode) {
switch (viewMode) {
case ViewMode.mobile:
@@ -126,33 +48,104 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
@override
Widget build(BuildContext context) {
return FloatLayout(
floatingWidget: Container(
margin: const EdgeInsets.all(kFloatingActionButtonMargin),
child: FloatingActionButton(
_initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
commonScaffoldState.loadingRun<void>(
() async {
await globalState.appController.updateProfiles();
},
);
},
icon: const Icon(Icons.sync),
),
const SizedBox(
width: 8,
)
];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(Icons.add),
),
),
child: const Icon(
Icons.add,
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState();
}
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode),
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
final columns = _getColumns(state.viewMode);
final isMobile = state.viewMode == ViewMode.mobile;
return Align(
alignment: Alignment.topCenter,
child: _buildGrid(
state: state,
crossAxisCount: _getColumns(state.viewMode),
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
WidgetsBinding.instance.addPostFrameCallback((_) {
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
});
return true;
},
child: ValueListenableBuilder(
valueListenable: hasPadding,
builder: (_, hasPadding, __) {
return SingleChildScrollView(
padding: !isMobile
? EdgeInsets.only(
left: 16,
right: 16,
top: 16,
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,
),
),
],
),
);
},
),
),
);
},
@@ -161,92 +154,188 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
class ProfileItem extends StatelessWidget {
class ProfileItem extends StatefulWidget {
final Profile profile;
final String? groupValue;
final CommonPopupMenu commonPopupMenu;
final void Function(String? value) onChanged;
const ProfileItem({
super.key,
required this.profile,
required this.commonPopupMenu,
required this.groupValue,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
String useShow;
String totalShow;
double progress;
final userInfo = profile.userInfo;
if (userInfo == null) {
useShow = "Infinite";
totalShow = "Infinite";
progress = 1;
} else {
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
useShow = TrafficValue(value: use).show;
totalShow = TrafficValue(value: total).show;
progress = total == 0 ? 0.0 : use / total;
}
return ListItem.radio(
horizontalTitleGap: 16,
delegate: RadioDelegate<String?>(
value: profile.id,
groupValue: groupValue,
onChanged: onChanged,
State<ProfileItem> createState() => _ProfileItemState();
}
class _ProfileItemState extends State<ProfileItem> {
final isUpdating = ValueNotifier<bool>(false);
_handleDeleteProfile(String id) async {
globalState.appController.deleteProfile(id);
}
_handleUpdateProfile(String id) async {
isUpdating.value = true;
await globalState.safeRun<void>(() async {
await globalState.appController.updateProfile(id);
});
isUpdating.value = false;
}
_handleShowEditExtendPage(
Profile profile,
) {
showExtendPage(
context,
body: EditProfile(
profile: profile.copyWith(),
context: context,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: commonPopupMenu,
title: Column(
mainAxisSize: MainAxisSize.min,
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
}
_buildTitle(Profile profile) {
final textTheme = context.textTheme;
final userInfo = profile.userInfo ?? 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
? "长期有效"
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
return Container(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
profile.label ?? profile.id,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
style: Theme.of(context).textTheme.labelMedium?.toLight(),
),
),
],
),
),
Flexible(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 8,
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
profile.label ?? profile.id,
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
child: LinearProgressIndicator(
minHeight: 6,
value: progress,
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
style: textTheme.labelMedium?.toLight(),
),
),
],
),
Flexible(
child: Text(
"$useShow / $totalShow",
style: Theme.of(context).textTheme.labelMedium?.toLight(),
),
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(
"到期时间:",
style: textTheme.labelMedium?.toLighter(),
),
const SizedBox(
width: 4,
),
Text(
expireShow,
style: textTheme.labelMedium?.toLighter(),
),
],
)
],
),
],
),
);
}
@override
Widget build(BuildContext context) {
final profile = widget.profile;
final groupValue = widget.groupValue;
final onChanged = widget.onChanged;
return Selector<AppState, ViewMode>(
selector: (_, appState) => appState.viewMode,
builder: (_, viewMode, child) {
if (viewMode == ViewMode.mobile) {
return child!;
}
return CommonCard(
child: child!,
);
},
child: ListItem.radio(
key: Key(profile.id),
horizontalTitleGap: 16,
delegate: RadioDelegate<String?>(
value: profile.id,
groupValue: groupValue,
onChanged: onChanged,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: 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(profile);
break;
case ProfileActions.delete:
_handleDeleteProfile(profile.id);
break;
case ProfileActions.update:
_handleUpdateProfile(profile.id);
break;
case null:
break;
}
},
),
title: _buildTitle(profile),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -50,6 +51,9 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
selectedValue: proxiesSortType,
);
},
),
const SizedBox(
width: 8,
)
];
});
@@ -57,72 +61,69 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
@override
Widget build(BuildContext context) {
return DelayTestButtonContainer(
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector3<AppState, Config, ClashConfig, ProxiesSelectorState>(
selector: (_, appState, config, clashConfig) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
);
},
child: Selector3<AppState, Config, ClashConfig, ProxiesSelectorState>(
selector: (_, appState, config, clashConfig) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
);
},
shouldRebuild: (prev, next) {
if (prev.groupNames.length != next.groupNames.length) {
_tabController?.dispose();
_tabController = null;
}
return prev != next;
},
builder: (_, state, __) {
_tabController ??= TabController(
length: state.groupNames.length,
vsync: this,
);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
shouldRebuild: (prev, next) {
if (prev.groupNames.length != next.groupNames.length) {
_tabController?.dispose();
_tabController = null;
}
return prev != next;
},
builder: (_, state, __) {
_tabController ??= TabController(
length: state.groupNames.length,
vsync: this,
);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
padding: const EdgeInsets.symmetric(horizontal: 16),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in state.groupNames)
Tab(
text: groupName,
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
padding: const EdgeInsets.symmetric(horizontal: 16),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor:
const WidgetStatePropertyAll(Colors.transparent),
tabs: [
children: [
for (final groupName in state.groupNames)
Tab(
text: groupName,
KeepContainer(
key: ObjectKey(groupName),
child: ProxiesTabView(
groupName: groupName,
),
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
for (final groupName in state.groupNames)
KeepContainer(
key: ObjectKey(groupName),
child: ProxiesTabView(
groupName: groupName,
),
),
],
),
)
],
);
},
),
)
],
);
},
),
);
}
@@ -196,6 +197,23 @@ class ProxiesTabView extends StatelessWidget {
}
}
_delayTest(List<Proxy> proxies) async {
for (final proxy in proxies) {
final appController = globalState.appController;
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);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesTabViewSelectorState>(
@@ -213,25 +231,32 @@ class ProxiesTabView extends StatelessWidget {
state.group.all,
state.proxiesSortType,
);
return Align(
alignment: Alignment.topCenter,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _getColumns(state.viewMode),
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(context),
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
state.group.all,
);
},
child: Align(
alignment: Alignment.topCenter,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _getColumns(state.viewMode),
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(context),
),
itemCount: proxies.length,
itemBuilder: (_, index) {
final proxy = proxies[index];
return ProxyCard(
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
),
itemCount: proxies.length,
itemBuilder: (_, index) {
final proxy = proxies[index];
return ProxyCard(
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
),
);
},
@@ -384,10 +409,12 @@ class ProxyCard extends StatelessWidget {
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
final Future Function() onClick;
const DelayTestButtonContainer({
super.key,
required this.child,
required this.onClick,
});
@override
@@ -401,12 +428,9 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
late Animation<double> _scale;
_healthcheck() async {
if (globalState.healthcheckLock) return;
_controller.forward();
globalState.appController.healthcheck();
Future.delayed(httpTimeoutDuration + moreDuration, () {
_controller.reverse();
});
await widget.onClick();
_controller.reverse();
}
@override
@@ -415,7 +439,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 600,
milliseconds: 200,
),
);
_scale = Tween<double>(
@@ -427,7 +451,6 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
curve: const Interval(
0,
1,
curve: Curves.elasticInOut,
),
),
);
@@ -441,6 +464,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
@override
Widget build(BuildContext context) {
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(

View File

@@ -156,5 +156,15 @@
"goDownload": "Go to download",
"unknown": "Unknown",
"geoData": "GeoData",
"externalResources": "External resources"
"externalResources": "External resources",
"checking": "Checking...",
"country": "Country",
"checkError": "Check error",
"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"
}

View File

@@ -156,5 +156,15 @@
"goDownload": "前往下载",
"unknown": "未知",
"geoData": "地理数据",
"externalResources": "外部资源"
"externalResources": "外部资源",
"checking": "检测中...",
"country": "区域",
"checkError": "检测失败",
"ipCheckTimeout": "Ip检测超时",
"search": "搜索",
"allowBypass": "允许应用绕过vpn",
"allowBypassDesc": "开启后部分应用可绕过VPN",
"externalController": "外部控制器",
"externalControllerDesc": "开启后可通过9090端口控制clash内核",
"ipv6Desc": "开启后将可以接收ipv6流量"
}

View File

@@ -40,6 +40,10 @@ class MessageLookup extends MessageLookupByLibrary {
"addressTip": MessageLookupByLibrary.simpleMessage(
"Please enter a valid WebDAV address"),
"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"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage(
"Allow access proxy through the LAN"),
@@ -76,10 +80,12 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Cancel filter system app"),
"cancelSelectAll":
MessageLookupByLibrary.simpleMessage("Cancel select all"),
"checkError": MessageLookupByLibrary.simpleMessage("Check error"),
"checkUpdate":
MessageLookupByLibrary.simpleMessage("Check for updates"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage(
"The current application is already the latest version"),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"compatible":
MessageLookupByLibrary.simpleMessage("Compatibility mode"),
"compatibleDesc": MessageLookupByLibrary.simpleMessage(
@@ -88,6 +94,7 @@ class MessageLookup extends MessageLookupByLibrary {
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"core": MessageLookupByLibrary.simpleMessage("Core"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
"country": MessageLookupByLibrary.simpleMessage("Country"),
"create": MessageLookupByLibrary.simpleMessage("Create"),
"dark": MessageLookupByLibrary.simpleMessage("Dark"),
"dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"),
@@ -109,6 +116,10 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
"externalController":
MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"Enabled to control the clash on port 9090"),
"externalResources":
MessageLookupByLibrary.simpleMessage("External resources"),
"file": MessageLookupByLibrary.simpleMessage("File"),
@@ -122,6 +133,10 @@ class MessageLookup extends MessageLookupByLibrary {
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
"importFromURL":
MessageLookupByLibrary.simpleMessage("Import from URL"),
"ipCheckTimeout":
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
"Enabled to will allow it to receive ipv6 traffic"),
"just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"light": MessageLookupByLibrary.simpleMessage("Light"),
@@ -207,6 +222,7 @@ class MessageLookup extends MessageLookupByLibrary {
"External resource related info"),
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
"selected": MessageLookupByLibrary.simpleMessage("Selected"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"),

View File

@@ -36,6 +36,9 @@ class MessageLookup extends MessageLookupByLibrary {
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"ago": MessageLookupByLibrary.simpleMessage(""),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过vpn"),
"allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
@@ -63,8 +66,10 @@ class MessageLookup extends MessageLookupByLibrary {
"cancelFilterSystemApp":
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc":
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"),
@@ -72,6 +77,7 @@ class MessageLookup extends MessageLookupByLibrary {
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"core": MessageLookupByLibrary.simpleMessage("内核"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"country": MessageLookupByLibrary.simpleMessage("区域"),
"create": MessageLookupByLibrary.simpleMessage("创建"),
"dark": MessageLookupByLibrary.simpleMessage("深色"),
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
@@ -90,6 +96,9 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后可通过9090端口控制clash内核"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
@@ -99,6 +108,8 @@ class MessageLookup extends MessageLookupByLibrary {
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hours": MessageLookupByLibrary.simpleMessage("小时"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
@@ -167,6 +178,7 @@ class MessageLookup extends MessageLookupByLibrary {
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("搜索"),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"),

View File

@@ -1629,6 +1629,106 @@ class AppLocalizations {
args: [],
);
}
/// `Checking...`
String get checking {
return Intl.message(
'Checking...',
name: 'checking',
desc: '',
args: [],
);
}
/// `Country`
String get country {
return Intl.message(
'Country',
name: 'country',
desc: '',
args: [],
);
}
/// `Check error`
String get checkError {
return Intl.message(
'Check error',
name: 'checkError',
desc: '',
args: [],
);
}
/// `Ip check timeout`
String get ipCheckTimeout {
return Intl.message(
'Ip check timeout',
name: 'ipCheckTimeout',
desc: '',
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> {

View File

@@ -22,16 +22,16 @@ Future<void> main() async {
isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
);
appState.navigationItems = navigation.getItems(
openLogs: config.openLogs,
hasProxies: false,
);
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
globalState.updateNavigationItems(
appState: appState,
config: config,
clashConfig: clashConfig,
);
runAppWithPreferences(
const Application(),
appState: appState,
@@ -61,6 +61,16 @@ Future<void> vpnService() async {
clashConfig: clashConfig,
);
if (appState.isInit) {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
} else {
exit(0);
}
final appLocalizations = await AppLocalizations.load(
other.getLocaleForString(config.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,

View File

@@ -89,6 +89,8 @@ class AppState with ChangeNotifier {
}
}
bool get isStart => _runTime != null;
int? get runTime => _runTime;
set runTime(int? value) {
@@ -105,7 +107,7 @@ class AppState with ChangeNotifier {
} else {
final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return type;
return "$type(${groups[index].now})";
return "$type(${groups[index].now ?? '*'})";
}
}
@@ -166,7 +168,7 @@ class AppState with ChangeNotifier {
addLog(Log log) {
_logs.add(log);
if(_logs.length > 60){
if (_logs.length > 60) {
_logs = _logs.sublist(_logs.length - 60);
}
notifyListeners();

View File

@@ -108,6 +108,8 @@ class Dns {
class ClashConfig extends ChangeNotifier {
int _mixedPort;
bool _allowLan;
bool _ipv6;
String _externalController;
Mode _mode;
LogLevel _logLevel;
Tun _tun;
@@ -118,15 +120,19 @@ class ClashConfig extends ChangeNotifier {
int? mixedPort,
Mode? mode,
bool? allowLan,
bool? ipv6,
LogLevel? logLevel,
String? externalController,
Tun? tun,
Dns? dns,
List<String>? rules,
}) : _mixedPort = mixedPort ?? 7890,
_mode = mode ?? Mode.rule,
_ipv6 = ipv6 ?? false,
_allowLan = allowLan ?? false,
_logLevel = logLevel ?? LogLevel.info,
_tun = tun ?? const Tun(),
_externalController = externalController ?? '',
_dns = dns ?? Dns(),
_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;
set tun(Tun value) {

View File

@@ -8,6 +8,7 @@ import '../common/common.dart';
import 'models.dart';
part 'generated/config.g.dart';
part 'generated/config.freezed.dart';
@freezed
@@ -23,6 +24,17 @@ class AccessControl with _$AccessControl {
_$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()
class Config extends ChangeNotifier {
List<Profile> _profiles;
@@ -41,6 +53,7 @@ class Config extends ChangeNotifier {
AccessControl _accessControl;
bool _isAnimateToPage;
bool _autoCheckUpdate;
bool _allowBypass;
DAV? _dav;
Config()
@@ -57,7 +70,8 @@ class Config extends ChangeNotifier {
_isAccessControl = false,
_autoCheckUpdate = true,
_accessControl = const AccessControl(),
_isAnimateToPage = true;
_isAnimateToPage = true,
_allowBypass = true;
deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList();
@@ -305,8 +319,22 @@ class Config extends ChangeNotifier {
}
}
update(
[Config? config, RecoveryOption recoveryOptions = RecoveryOption.all]) {
@JsonKey(defaultValue: true)
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) {
_profiles = config._profiles;
for (final profile in config._profiles) {
@@ -325,6 +353,7 @@ class Config extends ChangeNotifier {
_openLog = config._openLog;
_themeMode = config._themeMode;
_locale = config._locale;
_allowBypass = config._allowBypass;
_primaryColor = config._primaryColor;
_proxiesSortType = config._proxiesSortType;
_isMinimizeOnExit = config._isMinimizeOnExit;

View File

@@ -40,6 +40,7 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig(
mode: $enumDecodeNullable(_$ModeEnumMap, json['mode']),
allowLan: json['allow-lan'] as bool?,
logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']),
externalController: json['external-controller'] as String? ?? '',
tun: json['tun'] == null
? null
: Tun.fromJson(json['tun'] as Map<String, dynamic>),
@@ -56,6 +57,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'mode': _$ModeEnumMap[instance.mode]!,
'allow-lan': instance.allowLan,
'log-level': _$LogLevelEnumMap[instance.logLevel]!,
'external-controller': instance.externalController,
'tun': instance.tun,
'dns': instance.dns,
'rules': instance.rules,

View File

@@ -239,3 +239,172 @@ abstract class _AccessControl implements AccessControl {
_$$AccessControlImplCopyWith<_$AccessControlImpl> get copyWith =>
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;
}

View File

@@ -31,8 +31,9 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
? null
: DAV.fromJson(json['dav'] as Map<String, dynamic>)
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
..isCompatible = json['isCompatible'] as bool? ?? false
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true;
..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -52,6 +53,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'isAnimateToPage': instance.isAnimateToPage,
'isCompatible': instance.isCompatible,
'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass,
};
const _$ThemeModeEnumMap = {
@@ -93,3 +95,17 @@ const _$AccessControlModeEnumMap = {
AccessControlMode.acceptSelected: 'acceptSelected',
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,
};

View File

@@ -28,6 +28,8 @@ const _$GroupTypeEnumMap = {
GroupType.Selector: 'Selector',
GroupType.URLTest: 'URLTest',
GroupType.Fallback: 'Fallback',
GroupType.LoadBalance: 'LoadBalance',
GroupType.Relay: 'Relay',
};
_$ProxyImpl _$$ProxyImplFromJson(Map<String, dynamic> json) => _$ProxyImpl(

View File

@@ -157,34 +157,30 @@ abstract class _StartButtonSelectorState implements StartButtonSelectorState {
}
/// @nodoc
mixin _$UpdateCurrentDelaySelectorState {
String? get currentProxyName => throw _privateConstructorUsedError;
bool get isCurrent => throw _privateConstructorUsedError;
int? get delay => throw _privateConstructorUsedError;
mixin _$CheckIpSelectorState {
bool get isInit => throw _privateConstructorUsedError;
bool get isStart => throw _privateConstructorUsedError;
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$UpdateCurrentDelaySelectorStateCopyWith<UpdateCurrentDelaySelectorState>
get copyWith => throw _privateConstructorUsedError;
$CheckIpSelectorStateCopyWith<CheckIpSelectorState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UpdateCurrentDelaySelectorStateCopyWith<$Res> {
factory $UpdateCurrentDelaySelectorStateCopyWith(
UpdateCurrentDelaySelectorState value,
$Res Function(UpdateCurrentDelaySelectorState) then) =
_$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res,
UpdateCurrentDelaySelectorState>;
abstract class $CheckIpSelectorStateCopyWith<$Res> {
factory $CheckIpSelectorStateCopyWith(CheckIpSelectorState value,
$Res Function(CheckIpSelectorState) then) =
_$CheckIpSelectorStateCopyWithImpl<$Res, CheckIpSelectorState>;
@useResult
$Res call(
{String? currentProxyName, bool isCurrent, int? delay, bool isInit});
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap});
}
/// @nodoc
class _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res,
$Val extends UpdateCurrentDelaySelectorState>
implements $UpdateCurrentDelaySelectorStateCopyWith<$Res> {
_$UpdateCurrentDelaySelectorStateCopyWithImpl(this._value, this._then);
class _$CheckIpSelectorStateCopyWithImpl<$Res,
$Val extends CheckIpSelectorState>
implements $CheckIpSelectorStateCopyWith<$Res> {
_$CheckIpSelectorStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
@@ -194,154 +190,136 @@ class _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res,
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentProxyName = freezed,
Object? isCurrent = null,
Object? delay = freezed,
Object? isInit = null,
Object? isStart = null,
Object? selectedMap = null,
}) {
return _then(_value.copyWith(
currentProxyName: freezed == currentProxyName
? _value.currentProxyName
: currentProxyName // ignore: cast_nullable_to_non_nullable
as String?,
isCurrent: null == isCurrent
? _value.isCurrent
: isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
delay: freezed == delay
? _value.delay
: delay // ignore: cast_nullable_to_non_nullable
as int?,
isInit: null == isInit
? _value.isInit
: isInit // ignore: cast_nullable_to_non_nullable
as bool,
isStart: null == isStart
? _value.isStart
: isStart // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$UpdateCurrentDelaySelectorStateImplCopyWith<$Res>
implements $UpdateCurrentDelaySelectorStateCopyWith<$Res> {
factory _$$UpdateCurrentDelaySelectorStateImplCopyWith(
_$UpdateCurrentDelaySelectorStateImpl value,
$Res Function(_$UpdateCurrentDelaySelectorStateImpl) then) =
__$$UpdateCurrentDelaySelectorStateImplCopyWithImpl<$Res>;
abstract class _$$CheckIpSelectorStateImplCopyWith<$Res>
implements $CheckIpSelectorStateCopyWith<$Res> {
factory _$$CheckIpSelectorStateImplCopyWith(_$CheckIpSelectorStateImpl value,
$Res Function(_$CheckIpSelectorStateImpl) then) =
__$$CheckIpSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String? currentProxyName, bool isCurrent, int? delay, bool isInit});
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap});
}
/// @nodoc
class __$$UpdateCurrentDelaySelectorStateImplCopyWithImpl<$Res>
extends _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res,
_$UpdateCurrentDelaySelectorStateImpl>
implements _$$UpdateCurrentDelaySelectorStateImplCopyWith<$Res> {
__$$UpdateCurrentDelaySelectorStateImplCopyWithImpl(
_$UpdateCurrentDelaySelectorStateImpl _value,
$Res Function(_$UpdateCurrentDelaySelectorStateImpl) _then)
class __$$CheckIpSelectorStateImplCopyWithImpl<$Res>
extends _$CheckIpSelectorStateCopyWithImpl<$Res, _$CheckIpSelectorStateImpl>
implements _$$CheckIpSelectorStateImplCopyWith<$Res> {
__$$CheckIpSelectorStateImplCopyWithImpl(_$CheckIpSelectorStateImpl _value,
$Res Function(_$CheckIpSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentProxyName = freezed,
Object? isCurrent = null,
Object? delay = freezed,
Object? isInit = null,
Object? isStart = null,
Object? selectedMap = null,
}) {
return _then(_$UpdateCurrentDelaySelectorStateImpl(
currentProxyName: freezed == currentProxyName
? _value.currentProxyName
: currentProxyName // ignore: cast_nullable_to_non_nullable
as String?,
isCurrent: null == isCurrent
? _value.isCurrent
: isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
delay: freezed == delay
? _value.delay
: delay // ignore: cast_nullable_to_non_nullable
as int?,
return _then(_$CheckIpSelectorStateImpl(
isInit: null == isInit
? _value.isInit
: isInit // ignore: cast_nullable_to_non_nullable
as bool,
isStart: null == isStart
? _value.isStart
: isStart // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
));
}
}
/// @nodoc
class _$UpdateCurrentDelaySelectorStateImpl
implements _UpdateCurrentDelaySelectorState {
const _$UpdateCurrentDelaySelectorStateImpl(
{required this.currentProxyName,
required this.isCurrent,
required this.delay,
required this.isInit});
class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
const _$CheckIpSelectorStateImpl(
{required this.isInit,
required this.isStart,
required final Map<String, String> selectedMap})
: _selectedMap = selectedMap;
@override
final String? currentProxyName;
@override
final bool isCurrent;
@override
final int? delay;
@override
final bool isInit;
@override
final bool isStart;
final Map<String, String> _selectedMap;
@override
Map<String, String> get selectedMap {
if (_selectedMap is EqualUnmodifiableMapView) return _selectedMap;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_selectedMap);
}
@override
String toString() {
return 'UpdateCurrentDelaySelectorState(currentProxyName: $currentProxyName, isCurrent: $isCurrent, delay: $delay, isInit: $isInit)';
return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UpdateCurrentDelaySelectorStateImpl &&
(identical(other.currentProxyName, currentProxyName) ||
other.currentProxyName == currentProxyName) &&
(identical(other.isCurrent, isCurrent) ||
other.isCurrent == isCurrent) &&
(identical(other.delay, delay) || other.delay == delay) &&
(identical(other.isInit, isInit) || other.isInit == isInit));
other is _$CheckIpSelectorStateImpl &&
(identical(other.isInit, isInit) || other.isInit == isInit) &&
(identical(other.isStart, isStart) || other.isStart == isStart) &&
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap));
}
@override
int get hashCode =>
Object.hash(runtimeType, currentProxyName, isCurrent, delay, isInit);
int get hashCode => Object.hash(runtimeType, isInit, isStart,
const DeepCollectionEquality().hash(_selectedMap));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$UpdateCurrentDelaySelectorStateImplCopyWith<
_$UpdateCurrentDelaySelectorStateImpl>
get copyWith => __$$UpdateCurrentDelaySelectorStateImplCopyWithImpl<
_$UpdateCurrentDelaySelectorStateImpl>(this, _$identity);
_$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl>
get copyWith =>
__$$CheckIpSelectorStateImplCopyWithImpl<_$CheckIpSelectorStateImpl>(
this, _$identity);
}
abstract class _UpdateCurrentDelaySelectorState
implements UpdateCurrentDelaySelectorState {
const factory _UpdateCurrentDelaySelectorState(
{required final String? currentProxyName,
required final bool isCurrent,
required final int? delay,
required final bool isInit}) = _$UpdateCurrentDelaySelectorStateImpl;
abstract class _CheckIpSelectorState implements CheckIpSelectorState {
const factory _CheckIpSelectorState(
{required final bool isInit,
required final bool isStart,
required final Map<String, String> selectedMap}) =
_$CheckIpSelectorStateImpl;
@override
String? get currentProxyName;
@override
bool get isCurrent;
@override
int? get delay;
@override
bool get isInit;
@override
bool get isStart;
@override
Map<String, String> get selectedMap;
@override
@JsonKey(ignore: true)
_$$UpdateCurrentDelaySelectorStateImplCopyWith<
_$UpdateCurrentDelaySelectorStateImpl>
_$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

70
lib/models/ip.dart Normal file
View File

@@ -0,0 +1,70 @@
class IpInfo {
final String ip;
final String countryCode;
const IpInfo({
required this.ip,
required this.countryCode,
});
static IpInfo fromIpInfoIoJson(Map<String, dynamic> json) {
return switch (json) {
{
"ip": final String ip,
"country": final String country,
} =>
IpInfo(
ip: ip,
countryCode: country,
),
_ => throw const FormatException("invalid json"),
};
}
static IpInfo fromIpApiCoJson(Map<String, dynamic> json) {
return switch (json) {
{
"ip": final String ip,
"country_code": final String countryCode,
} =>
IpInfo(
ip: ip,
countryCode: countryCode,
),
_ => throw const FormatException("invalid json"),
};
}
static IpInfo fromIpSbJson(Map<String, dynamic> json) {
return switch (json) {
{
"ip": final String ip,
"country_code": final String countryCode,
} =>
IpInfo(
ip: ip,
countryCode: countryCode,
),
_ => throw const FormatException("invalid json"),
};
}
static IpInfo fromIpwhoIsJson(Map<String, dynamic> json) {
return switch (json) {
{
"ip": final String ip,
"country_code": final String countryCode,
} =>
IpInfo(
ip: ip,
countryCode: countryCode,
),
_ => throw const FormatException("invalid json"),
};
}
@override
String toString() {
return 'IpInfo{ip: $ip, countryCode: $countryCode}';
}
}

View File

@@ -12,4 +12,5 @@ export 'package.dart';
export 'ffi.dart';
export 'selector.dart';
export 'navigation.dart';
export 'dav.dart';
export 'dav.dart';
export 'ip.dart';

View File

@@ -16,16 +16,17 @@ class UserInfo {
int upload;
int download;
int total;
int? expire;
int expire;
UserInfo({
int? upload,
int? download,
int? total,
this.expire,
int? expire,
}) : upload = upload ?? 0,
download = download ?? 0,
total = total ?? 0;
total = total ?? 0,
expire = expire ?? 0;
Map<String, dynamic> toJson() {
return _$UserInfoToJson(this);
@@ -37,10 +38,10 @@ class UserInfo {
factory UserInfo.formHString(String? info) {
if (info == null) return UserInfo();
var list = info.split(";");
final list = info.split(";");
Map<String, int?> map = {};
for (var i in list) {
var keyValue = i.trim().split("=");
for (final i in list) {
final keyValue = i.trim().split("=");
map[keyValue[0]] = int.tryParse(keyValue[1]);
}
return UserInfo(
@@ -83,7 +84,8 @@ class Profile {
autoUpdateDuration = autoUpdateDuration ?? defaultUpdateDuration,
selectedMap = selectedMap ?? {};
ProfileType get type => url == null ? ProfileType.file : ProfileType.url;
ProfileType get type =>
url == null || url?.isEmpty == true ? ProfileType.file : ProfileType.url;
Future<void> checkAndUpdate() async {
final isExists = await check();
@@ -95,9 +97,6 @@ class Profile {
}
Future<void> update() async {
if (url == null) {
throw appLocalizations.unableToUpdateCurrentProfileDesc;
}
final response = await request.getFileResponseForUrl(url!);
final disposition = response.headers.value("content-disposition");
label ??= other.getFileNameForDisposition(disposition) ?? id;

View File

@@ -1,10 +1,7 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'config.dart';
import 'navigation.dart';
import 'profile.dart';
import 'proxy.dart';
part 'generated/selector.freezed.dart';
@@ -17,13 +14,12 @@ class StartButtonSelectorState with _$StartButtonSelectorState {
}
@freezed
class UpdateCurrentDelaySelectorState with _$UpdateCurrentDelaySelectorState {
const factory UpdateCurrentDelaySelectorState({
required String? currentProxyName,
required bool isCurrent,
required int? delay,
class CheckIpSelectorState with _$CheckIpSelectorState {
const factory CheckIpSelectorState({
required bool isInit,
}) = _UpdateCurrentDelaySelectorState;
required bool isStart,
required SelectedMap selectedMap,
}) = _CheckIpSelectorState;
}
@freezed
@@ -53,24 +49,23 @@ class ApplicationSelectorState with _$ApplicationSelectorState {
}
@freezed
class TrayContainerSelectorState with _$TrayContainerSelectorState{
class TrayContainerSelectorState with _$TrayContainerSelectorState {
const factory TrayContainerSelectorState({
required Mode mode,
required bool autoLaunch,
required bool isRun,
required String? locale,
})=_TrayContainerSelectorState;
}) = _TrayContainerSelectorState;
}
@freezed
class UpdateNavigationsSelector with _$UpdateNavigationsSelector{
class UpdateNavigationsSelector with _$UpdateNavigationsSelector {
const factory UpdateNavigationsSelector({
required bool openLogs,
required bool hasProxies,
}) = _UpdateNavigationsSelector;
}
@freezed
class HomeSelectorState with _$HomeSelectorState {
const factory HomeSelectorState({
@@ -89,21 +84,21 @@ class HomeBodySelectorState with _$HomeBodySelectorState {
}
@freezed
class ProxiesCardSelectorState with _$ProxiesCardSelectorState{
class ProxiesCardSelectorState with _$ProxiesCardSelectorState {
const factory ProxiesCardSelectorState({
required bool isSelected,
}) = _ProxiesCardSelectorState;
}
@freezed
class ProxiesSelectorState with _$ProxiesSelectorState{
class ProxiesSelectorState with _$ProxiesSelectorState {
const factory ProxiesSelectorState({
required List<String> groupNames,
}) = _ProxiesSelectorState;
}
@freezed
class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState{
class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState {
const factory ProxiesTabViewSelectorState({
required ProxiesSortType proxiesSortType,
required num sortNum,
@@ -125,4 +120,4 @@ class PackageListSelectorState with _$PackageListSelectorState {
required AccessControl accessControl,
required bool isAccessControl,
}) = _PackageListSelectorState;
}
}

View File

@@ -107,6 +107,9 @@ class _ScanPageState extends State<ScanPage> with WidgetsBindingObserver {
case TorchState.unavailable:
icon = const Icon(Icons.flash_off);
backgroundColor = Colors.transparent;
case TorchState.auto:
icon = const Icon(Icons.flash_auto);
backgroundColor = Colors.orange;
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),

View File

@@ -13,7 +13,6 @@ import 'common/common.dart';
class GlobalState {
Timer? timer;
Function? healthcheckLockDebounce;
Timer? groupsUpdateTimer;
Function? updateCurrentDelayDebounce;
PageController? pageController;
@@ -22,7 +21,6 @@ class GlobalState {
late AppController appController;
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
List<Function> updateFunctionLists = [];
bool healthcheckLock = false;
startListenUpdate() {
if (timer != null && timer!.isActive == true) return;
@@ -63,8 +61,14 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) async {
final args =
config.isAccessControl ? json.encode(config.accessControl) : null;
final args = config.isAccessControl
? json.encode(
Props(
accessControl: config.accessControl,
allowBypass: config.allowBypass,
),
)
: null;
await proxyManager.startProxy(
port: clashConfig.mixedPort,
args: args,
@@ -107,12 +111,6 @@ class GlobalState {
clashConfig: clashConfig,
);
}
if (!appState.isInit) return;
await applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
updateCoreVersionInfo(appState);
}
@@ -121,35 +119,20 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (config.profiles.isEmpty) {
stopSystemProxy();
return;
}
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: key,
proxyName: value,
),
);
});
if (config.profiles.isEmpty) {
stopSystemProxy();
return;
}
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: key,
proxyName: value,
),
);
});
}
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 {
appState.groups = await clashCore.getProxiesGroups();
}
@@ -255,20 +238,6 @@ class GlobalState {
);
}
void updateCurrentDelay(
String? proxyName,
) {
updateCurrentDelayDebounce ??= debounce<Function(String?)>((proxyName) {
if (proxyName != null) {
debugPrint("[delay]=====> $proxyName");
clashCore.delay(
proxyName,
);
}
});
updateCurrentDelayDebounce!([proxyName]);
}
Future<T?> safeRun<T>(
FutureOr<T> Function() futureFunction, {
String? title,

View File

@@ -38,16 +38,8 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
@override
void onDelay(Delay delay) {
globalState.healthcheckLock = true;
final appController = globalState.appController;
appController.setDelay(delay);
globalState.healthcheckLockDebounce ??= debounce<Function()>(
() async {
globalState.healthcheckLock = false;
},
milliseconds: 5000,
);
globalState.healthcheckLockDebounce!();
super.onDelay(delay);
}

View File

@@ -71,6 +71,7 @@ class ListItem<T> extends StatelessWidget {
final Widget title;
final Widget? subtitle;
final EdgeInsets padding;
final ListTileTitleAlignment tileTitleAlignment;
final bool? prue;
final Widget? trailing;
final Delegate delegate;
@@ -87,6 +88,7 @@ class ListItem<T> extends StatelessWidget {
this.horizontalTitleGap,
this.prue,
this.onTab,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : delegate = const Delegate();
const ListItem.open({
@@ -99,6 +101,7 @@ class ListItem<T> extends StatelessWidget {
required OpenDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTab = null;
const ListItem.next({
@@ -111,6 +114,7 @@ class ListItem<T> extends StatelessWidget {
required NextDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTab = null;
const ListItem.checkbox({
@@ -122,6 +126,7 @@ class ListItem<T> extends StatelessWidget {
required CheckboxDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTab = null;
@@ -134,6 +139,7 @@ class ListItem<T> extends StatelessWidget {
required SwitchDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTab = null;
@@ -146,6 +152,7 @@ class ListItem<T> extends StatelessWidget {
required RadioDelegate<T> this.delegate,
this.horizontalTitleGap = 8,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : leading = null,
onTab = null;
@@ -193,6 +200,7 @@ class ListItem<T> extends StatelessWidget {
horizontalTitleGap: horizontalTitleGap,
title: title,
subtitle: subtitle,
titleAlignment: tileTitleAlignment,
onTap: onTab,
trailing: trailing ?? this.trailing,
contentPadding: padding,

View File

@@ -49,6 +49,7 @@ class CommonScaffold extends StatefulWidget {
class CommonScaffoldState extends State<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loading = ValueNotifier(false);
@@ -58,11 +59,16 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
set floatingActionButton(Widget? actions) {
if (_floatingActionButton.value != actions) {
_floatingActionButton.value = actions;
}
}
Future<T?> loadingRun<T>(
Future<T> Function() futureFunction, {
String? title,
}) async {
if (_loading.value == true) return null;
_loading.value = true;
try {
final res = await futureFunction();
@@ -85,6 +91,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
_actions.value = [];
_floatingActionButton.value = null;
}
}
@@ -109,6 +116,12 @@ class CommonScaffoldState extends State<CommonScaffold> {
Widget build(BuildContext context) {
return _platformContainer(
child: Scaffold(
floatingActionButton: ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, floatingActionButton, __) {
return floatingActionButton ?? Container();
},
),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none'
version: 0.8.10
version: 0.8.20
environment:
sdk: '>=3.1.0 <4.0.0'
@@ -17,14 +17,14 @@ dependencies:
provider: ^6.0.5
window_manager: ^0.3.8
ffi: ^2.1.0
dynamic_color: ^1.7.0
dynamic_color: ^1.6.0
proxy:
path: plugins/proxy
launch_at_startup: ^0.2.2
windows_single_instance: ^1.0.1
json_annotation: ^4.9.0
file_picker: ^8.0.3
mobile_scanner: 5.0.1
mobile_scanner: ^5.1.1
app_links: ^3.5.0
win32_registry: ^1.1.2
tray_manager: ^0.2.1
@@ -38,6 +38,7 @@ dependencies:
image: ^4.1.7
webdav_client: ^1.2.2
dio: ^5.4.3+1
country_flags: ^2.2.0
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -12,10 +12,7 @@ enum PlatformType {
macos,
}
enum Arch {
amd64,
arm64,
}
enum Arch { amd64, arm64, arm }
class BuildLibItem {
PlatformType platform;
@@ -64,6 +61,11 @@ class Build {
arch: Arch.amd64,
archName: 'amd64',
),
BuildLibItem(
platform: PlatformType.android,
arch: Arch.arm,
archName: 'armeabi-v7a',
),
BuildLibItem(
platform: PlatformType.android,
arch: Arch.arm64,
@@ -334,7 +336,7 @@ class BuildCommand extends Command {
final archName = argResults?['arch'];
final currentArches =
arches.where((element) => element.name == archName).toList();
final arch = currentArches.isEmpty ? null : arches.first;
final arch = currentArches.isEmpty ? null : currentArches.first;
await _buildLib(arch);
if (build != "all") {
return;
@@ -357,10 +359,11 @@ class BuildCommand extends Command {
break;
case PlatformType.android:
final targetMap = {
Arch.arm: "android-arm",
Arch.arm64: "android-arm64",
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
.where((element) => arch == null ? true : element == arch)
.map((e) => targetMap[e])