Compare commits

...

12 Commits

Author SHA1 Message Date
chen08209
afa1b4f424 Update version 2024-06-19 10:12:21 +08:00
chen08209
fa67940ec9 Fix the problem of excessive memory usage in traffic usage.
Add lightBlue theme color

Fix start unable to update profile issues
2024-06-19 10:10:41 +08:00
chen08209
90bb670442 Fix flashback caused by process 2024-06-17 15:49:16 +08:00
chen08209
05abf2d56d Add build version
Optimize quick start

Update system default option
2024-06-16 16:48:52 +08:00
chen08209
658727dd79 Update build.yml 2024-06-16 13:18:55 +08:00
chen08209
f7abf6446c Fix android vpn close issues
Add requests page

Fix checkUpdate dark mode style error

Fix quickStart error open app

Add memory proxies tab index

Support hidden group

Optimize logs
2024-06-16 13:06:34 +08:00
chen08209
5ab4dd0cbd Fix externalController hot load error 2024-06-13 19:22:26 +08:00
chen08209
35f7279fcb Add tcp concurrent switch
Add system proxy switch

Add geodata loader switch

Add external controller switch

Add auto gc on trim memory

Fix android notification error
2024-06-13 17:04:57 +08:00
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
77 changed files with 2742 additions and 864 deletions

View File

@@ -17,10 +17,16 @@ jobs:
os: windows-latest
- platform: linux
os: ubuntu-latest
# - platform: macos
# os: macos-13
- platform: macos
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:

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

@@ -1,6 +1,7 @@
package com.follow.clash
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.engine.FlutterEngine
import java.util.Date
@@ -18,6 +19,9 @@ class GlobalState {
var flutterEngine: FlutterEngine? = null
fun getCurrentTilePlugin(): TilePlugin? =
flutterEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
fun getCurrentAppPlugin(): AppPlugin? =
flutterEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
}

View File

@@ -1,17 +1,18 @@
package com.follow.clash.extensions
import java.net.InetSocketAddress
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
import java.net.URL
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.models.Metadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.InetAddress
import java.net.InetSocketAddress
suspend fun Drawable.getBase64(): String {
@@ -29,3 +30,8 @@ fun Metadata.getProtocol(): Int? {
if (network.startsWith("udp")) return IPPROTO_UDP
return null
}
fun String.getInetSocketAddress(): InetSocketAddress {
val url = URL("https://$this")
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
}

View File

@@ -1,11 +1,15 @@
package com.follow.clash.models
data class Process(
val id: Int,
val metadata: Metadata,
)
data class Metadata(
val network: String,
val sourceIP: String,
val sourcePort: Int,
val destinationIP: String,
val destinationPort: Int,
val remoteDestination: String,
val host: String
)

View File

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

View File

@@ -6,13 +6,16 @@ import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.extensions.getBase64
import com.follow.clash.extensions.getInetSocketAddress
import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Metadata
import com.follow.clash.models.Process
import com.follow.clash.models.Package
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
@@ -78,21 +81,34 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
"getPackageIcon" -> {
scope.launch {
val packageName = call.argument<String>("packageName")
if (packageName != null) {
result.success(getPackageIcon(packageName))
} else {
if (packageName == null) {
result.success(null)
return@launch
}
val packageIcon = getPackageIcon(packageName)
packageIcon.let {
if (it != null) {
result.success(it)
return@launch
}
if (iconMap["default"] == null) {
iconMap["default"] =
context?.packageManager?.defaultActivityIcon?.getBase64()
}
result.success(iconMap["default"])
return@launch
}
}
}
"getPackageName" -> {
"resolverProcess" -> {
val data = call.argument<String>("data")
val metadata =
val process =
if (data != null) Gson().fromJson(
data,
Metadata::class.java
Process::class.java
) else null
val metadata = process?.metadata
val protocol = metadata?.getProtocol()
if (protocol == null) {
result.success(null)
@@ -100,17 +116,27 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
scope.launch {
withContext(Dispatchers.Default) {
if (context == null) result.success(null)
val source = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val target = InetSocketAddress(
metadata.host.ifEmpty { metadata.destinationIP },
metadata.destinationPort
)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q){
result.success(null)
return@withContext
}
if (context == null) {
result.success(null)
return@withContext
}
if (connectivity == null) {
connectivity = context!!.getSystemService<ConnectivityManager>()
}
val uid =
connectivity?.getConnectionOwnerUid(protocol, source, target)
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
metadata.destinationIP.ifEmpty { metadata.host },
metadata.destinationPort
)
val uid = try {
connectivity?.getConnectionOwnerUid(protocol, src, dst)
} catch (_: Exception) {
null
}
if (uid == null || uid == -1) {
result.success(null)
return@withContext
@@ -137,13 +163,18 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context?.packageManager
if (iconMap[packageName] == null) {
iconMap[packageName] = packageManager?.getApplicationIcon(packageName)?.getBase64()
iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64()
} catch (_: Exception) {
null
}
}
return iconMap[packageName]
}
private suspend fun getPackages(): String {
return withContext(Dispatchers.Default){
return withContext(Dispatchers.Default) {
val packageManager = context?.packageManager
val packages: List<Package>? =
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
@@ -162,6 +193,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
fun requestGc() {
channel.invokeMethod("gc", null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity;
scope = CoroutineScope(Dispatchers.Default)

View File

@@ -16,7 +16,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.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 +41,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 +73,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 +121,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

@@ -44,7 +44,7 @@ class FlClashTileService : TileService() {
private fun activityTransfer() {
val intent = Intent(this, TempActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,

View File

@@ -15,8 +15,8 @@ import androidx.core.app.NotificationCompat
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
class FlClashVpnService : VpnService() {
@@ -51,12 +51,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 {
@@ -77,8 +77,10 @@ class FlClashVpnService : VpnService() {
if (Build.VERSION.SDK_INT >= 29) {
setMetered(false)
}
allowBypass()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (props?.allowBypass == true) {
allowBypass()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1",
@@ -132,20 +134,26 @@ class FlClashVpnService : VpnService() {
}
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentAppPlugin()?.requestGc()
}
fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
channel.setShowBadge(false)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
}else{
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}

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

@@ -2,25 +2,24 @@ package main
import "C"
import (
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
ap "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"math"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
)
@@ -83,10 +82,13 @@ type Delay struct {
}
type Process struct {
Uid uint32 `json:"uid"`
Network string `json:"network"`
Source string `json:"source"`
Target string `json:"target"`
Id int64 `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
type ProcessMapItem struct {
Id int64 `json:"id"`
Value string `json:"value"`
}
type Now struct {
@@ -321,33 +323,35 @@ 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.TCPConcurrent = patchConfig.TCPConcurrent
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
targetConfig.GeodataMode = false
//targetConfig.IPv6 = patchConfig.IPv6
targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
targetConfig.SocksPort = 0
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.FindProcessMode = process.FindProcessAlways
targetConfig.FindProcessMode = patchConfig.FindProcessMode
targetConfig.AllowLan = patchConfig.AllowLan
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.GeodataLoader = "standard"
targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false
if targetConfig.DNS.Enable == false {
targetConfig.DNS = patchConfig.DNS
}
if runtime.GOOS == "android" {
targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
} else if runtime.GOOS == "windows" {
targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
}
//if runtime.GOOS == "android" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
//} else if runtime.GOOS == "windows" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
//}
if compatible == false {
targetConfig.ProxyProvider = make(map[string]map[string]any)
targetConfig.RuleProvider = make(map[string]map[string]any)
@@ -357,14 +361,17 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
func patchConfig(general *config.General) {
log.Infoln("[Apply] patch")
route.ReStartServer(general.ExternalController)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
inbound.SetAllowedIPs(general.LanAllowedIPs)
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
listener.SetBindAddress(general.BindAddress)
tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay)
listener.ReCreateHTTP(general.Port, tunnel.Tunnel)
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
@@ -377,31 +384,10 @@ func patchConfig(general *config.General) {
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6
}
const concurrentCount = math.MaxInt
func hcCompatibleProvider(proxyProviders map[string]provider.ProxyProvider) {
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == provider.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
}
}()
}
}
}
func applyConfig(isPatch bool) {
cfg, err := config.ParseRawConfig(currentConfig)
if err != nil {
@@ -410,7 +396,6 @@ func applyConfig(isPatch bool) {
if isPatch {
patchConfig(cfg.General)
} else {
executor.ApplyConfig(cfg, true)
hcCompatibleProvider(tunnel.Providers())
hub.UltraApplyConfig(cfg, true)
}
}

View File

@@ -12,6 +12,8 @@ const (
Delay MessageType = "delay"
Now MessageType = "now"
Process MessageType = "process"
Request MessageType = "request"
Run MessageType = "run"
)
type Message struct {
@@ -19,11 +21,18 @@ type Message struct {
Data interface{} `json:"data"`
}
func (message *Message) Json() string {
data, _ := json.Marshal(message)
return string(data)
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
func SendMessage(message Message) {
SendToPort(*Port, message.Json())
if Port == nil {
return
}
s, err := message.Json()
if err != nil {
return
}
SendToPort(*Port, s)
}

View File

@@ -1,6 +1,6 @@
module core
go 1.20
go 1.21.0
replace github.com/metacubex/mihomo => ./Clash.Meta
@@ -17,6 +17,7 @@ require (
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
@@ -30,6 +31,9 @@ require (
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/go-chi/cors v1.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect

View File

@@ -9,6 +9,8 @@ github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -35,16 +37,25 @@ 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-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
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=
@@ -60,6 +71,7 @@ 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=
@@ -69,6 +81,7 @@ 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=
@@ -83,7 +96,9 @@ 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=
@@ -125,6 +140,7 @@ 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=
@@ -146,6 +162,7 @@ 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=
@@ -249,6 +266,7 @@ 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=
@@ -263,6 +281,7 @@ 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

@@ -60,6 +60,14 @@ func shutdownClash() bool {
return true
}
//export forceGc
func forceGc() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
}()
}
//export validateConfig
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
@@ -180,6 +188,21 @@ func getTraffic() *C.char {
return C.CString(string(data))
}
//export getTotalTraffic
func getTotalTraffic() *C.char {
up, down := statistic.DefaultManager.Total()
traffic := map[string]int64{
"up": up,
"down": down,
}
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export asyncTestDelay
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
@@ -396,4 +419,10 @@ func init() {
Data: delayData,
})
}
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
bridge.SendMessage(bridge.Message{
Type: bridge.Request,
Data: c,
})
}
}

View File

@@ -18,6 +18,9 @@ func startLog() {
logSubscriber = log.Subscribe()
go func() {
for logData := range logSubscriber {
if logData.LogLevel < log.Level() {
continue
}
message := &bridge.Message{
Type: bridge.Log,
Data: logData,

View File

@@ -1,3 +1,82 @@
//go:build android
package main
import "C"
import (
bridge "core/dart-bridge"
"encoding/json"
"errors"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"sync"
"sync/atomic"
"time"
)
type ProcessMap struct {
m sync.Map
}
func (cm *ProcessMap) Store(key int64, value string) {
cm.m.Store(key, value)
}
func (cm *ProcessMap) Load(key int64) (string, bool) {
value, ok := cm.m.Load(key)
if !ok || value == nil {
return "", false
}
return value.(string), true
}
var counter int64 = 0
var processMap ProcessMap
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil {
return "", process.ErrInvalidNetwork
}
id := atomic.AddInt64(&counter, 1)
timeout := time.After(200 * time.Millisecond)
bridge.SendMessage(bridge.Message{
Type: bridge.Process,
Data: Process{
Id: id,
Metadata: metadata,
},
})
for {
select {
case <-timeout:
return "", errors.New("package resolver timeout")
default:
value, exists := processMap.Load(id)
if exists {
return value, nil
}
time.Sleep(20 * time.Millisecond)
}
}
}
}
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
go func() {
paramsString := C.GoString(s)
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(paramsString), processMapItem)
if err == nil {
processMap.Store(processMapItem.Id, processMapItem.Value)
}
}()
}

View File

@@ -35,8 +35,6 @@ func startTUN(fd C.int) {
closer, err := t.Start(f, gateway, portal, dns)
applyConfig(true)
if err != nil {
log.Errorln("startTUN error: %v", err)
tempTun.Close()
@@ -48,16 +46,6 @@ func startTUN(fd C.int) {
}()
}
//export updateMarkSocketPort
func updateMarkSocketPort(markSocketPort C.longlong) bool {
tunLock.Lock()
defer tunLock.Unlock()
//if tun != nil {
// tun.MarkSocketPort = int64(markSocketPort)
//}
return true
}
//export stopTun
func stopTun() {
go func() {
@@ -66,7 +54,6 @@ func stopTun() {
if tun != nil {
tun.Close()
applyConfig(true)
tun = nil
}
}()

View File

@@ -82,7 +82,11 @@ class ApplicationState extends State<Application> {
super.initState();
globalState.appController = AppController(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
globalState.appController.afterInit();
final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) {
globalState.appController = AppController(currentContext);
}
await globalState.appController.init();
globalState.appController.initLink();
_updateGroups();
});

View File

@@ -46,8 +46,8 @@ class ClashCore {
bool init(String homeDir) {
return clashFFI.initClash(
homeDir.toNativeUtf8().cast(),
) ==
homeDir.toNativeUtf8().cast(),
) ==
1;
}
@@ -99,8 +99,9 @@ class ClashCore {
final groupNames = [
UsedProxy.GLOBAL.name,
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
final proxy = proxies[e];
return GroupTypeExtension.valueList.contains(proxy['type']);
final proxy = proxies[e] ?? {};
return GroupTypeExtension.valueList.contains(proxy['type']) &&
proxy['hidden'] != true;
})
];
final groupsRaw = groupNames.map((groupName) {
@@ -108,7 +109,7 @@ class ClashCore {
group["all"] = ((group["all"] ?? []) as List)
.map(
(name) => proxies[name],
)
)
.toList();
return group;
}).toList();
@@ -119,14 +120,14 @@ class ClashCore {
Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
externalProvidersRaw.cast<Utf8>().toDartString();
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
});
}
@@ -198,6 +199,13 @@ class ClashCore {
return Traffic.fromMap(trafficMap);
}
Traffic getTotalTraffic() {
final trafficRaw = clashFFI.getTotalTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
return Traffic.fromMap(trafficMap);
}
void startLog() {
clashFFI.startLog();
}
@@ -210,14 +218,28 @@ class ClashCore {
clashFFI.startTUN(fd);
}
requestGc() {
clashFFI.forceGc();
}
void stopTun() {
clashFFI.stopTun();
}
void setProcessMap(ProcessMapItem processMapItem) {
clashFFI.setProcessMap(json.encode(processMapItem).toNativeUtf8().cast());
}
// DateTime? getRunTime() {
// final runTimeString = clashFFI.getRunTime().cast<Utf8>().toDartString();
// if (runTimeString.isEmpty) return null;
// return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
// }
List<Connection> getConnections() {
final connectionsDataRaw = clashFFI.getConnections();
final connectionsData =
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}

View File

@@ -893,6 +893,14 @@ class ClashFFI {
_lookup<ffi.NativeFunction<GoUint8 Function()>>('shutdownClash');
late final _shutdownClash = _shutdownClashPtr.asFunction<int Function()>();
void forceGc() {
return _forceGc();
}
late final _forceGcPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
void validateConfig(
ffi.Pointer<ffi.Char> s,
int port,
@@ -975,6 +983,16 @@ class ClashFFI {
late final _getTraffic =
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getTotalTraffic() {
return _getTotalTraffic();
}
late final _getTotalTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getTotalTraffic');
late final _getTotalTraffic =
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void asyncTestDelay(
ffi.Pointer<ffi.Char> s,
int port,
@@ -1122,7 +1140,21 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
int startTUN(
void setProcessMap(
ffi.Pointer<ffi.Char> s,
) {
return _setProcessMap(
s,
);
}
late final _setProcessMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setProcessMap');
late final _setProcessMap =
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void startTUN(
int fd,
) {
return _startTUN(
@@ -1131,22 +1163,8 @@ class ClashFFI {
}
late final _startTUNPtr =
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Int)>>('startTUN');
late final _startTUN = _startTUNPtr.asFunction<int Function(int)>();
int updateMarkSocketPort(
int markSocketPort,
) {
return _updateMarkSocketPort(
markSocketPort,
);
}
late final _updateMarkSocketPortPtr =
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.LongLong)>>(
'updateMarkSocketPort');
late final _updateMarkSocketPort =
_updateMarkSocketPortPtr.asFunction<int Function(int)>();
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>('startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int)>();
void stopTun() {
return _stopTun();

View File

@@ -14,9 +14,13 @@ abstract mixin class ClashMessageListener {
void onDelay(Delay delay) {}
void onProcess(Metadata metadata) {}
void onProcess(Process process) {}
void onRequest(Connection connection) {}
void onNow(Now now) {}
void onRun(String runTime) {}
}
class ClashMessage {
@@ -41,11 +45,17 @@ class ClashMessage {
listener.onDelay(Delay.fromJson(m.data));
break;
case MessageType.process:
listener.onProcess(Metadata.fromJson(m.data));
listener.onProcess(Process.fromJson(m.data));
break;
case MessageType.now:
listener.onNow(Now.fromJson(m.data));
break;
case MessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case MessageType.run:
listener.onRun(m.data);
break;
}
}
});

View File

@@ -7,6 +7,7 @@ class Android {
init() async {
app?.onExit = () {
clashCore.shutdown();
print("adsadda==>");
exit(0);
};
}

View File

@@ -18,8 +18,11 @@ 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;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
final filter = ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,

View File

@@ -29,6 +29,13 @@ class Navigation {
label: "profiles",
fragment: ProfilesFragment(),
),
const NavigationItem(
icon: Icon(Icons.ballot),
label: "requests",
fragment: RequestFragment(),
description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
const NavigationItem(
icon: Icon(Icons.swap_vert_circle),
label: "resources",

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

@@ -31,6 +31,7 @@ class AppController {
Future<void> updateSystemProxy(bool isStart) async {
if (isStart) {
await globalState.startSystemProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
@@ -145,7 +146,16 @@ class AppController {
if (isNotNeedUpdate == false || profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile.id);
try {
await updateProfile(profile.id);
} catch (e) {
appState.addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
}
}
}
@@ -222,20 +232,21 @@ class AppController {
final tagName = data['tag_name'];
final body = data['body'];
final submits = other.parseReleaseBody(body);
final textTheme = context.textTheme;
globalState.showMessage(
title: appLocalizations.discoverNewVersion,
message: TextSpan(
text: "$tagName \n",
style: context.textTheme.headlineSmall,
style: textTheme.headlineSmall,
children: [
TextSpan(
text: "\n",
style: context.textTheme.bodyMedium,
style: textTheme.bodyMedium,
),
for (final submit in submits)
TextSpan(
text: "- $submit \n",
style: context.textTheme.bodyMedium,
style: textTheme.bodyMedium,
),
],
),
@@ -256,6 +267,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,
);
},title: appLocalizations.init);
} else {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
await afterInit();
}
afterInit() async {
if (config.autoRun) {
await updateSystemProxy(true);
@@ -265,9 +299,6 @@ class AppController {
}
autoUpdateProfiles();
updateLogStatus();
if (!config.silentLaunch) {
window?.show();
}
autoCheckUpdate();
}

View File

@@ -56,7 +56,9 @@ enum ProfileType { file, url }
enum ResultType { success, error }
enum MessageType { log, tun, delay, process, now }
enum MessageType { log, tun, delay, process, now, request, run }
enum FindProcessMode { always, off }
enum RecoveryOption {
all,

View File

@@ -105,49 +105,56 @@ 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),
// );
// },
// );
// }
_buildSelectedAllButton({
required bool isAccessControl,
required bool isSelectedAll,
required List<String> allValueList,
}) {
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: [],
),
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
child: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
),
),
);
});
}
Widget _buildPackageList() {
return ValueListenableBuilder(
@@ -199,6 +206,11 @@ 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(
@@ -490,7 +502,8 @@ class AccessControlSearchDelegate extends SearchDelegate {
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final currentList = accessControl.currentList;
final packageNameList = this.packages.map((e) => e.packageName).toList();
final packageNameList =
this.packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
return DisabledMask(
status: !isAccessControl,
@@ -530,12 +543,11 @@ class AccessControlSearchDelegate extends SearchDelegate {
@override
Widget buildResults(BuildContext context) {
return _packageList(packages);
return buildSuggestions(context);
}
@override
Widget buildSuggestions(BuildContext context) {
final packages = _results;
return _packageList(packages);
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';
@@ -37,16 +39,132 @@ class _ConfigFragmentState extends State<ConfigFragment> {
}
}
_updateLoglevel(LogLevel? logLevel) {
if (logLevel == null ||
logLevel == globalState.appController.clashConfig.logLevel) return;
globalState.appController.clashConfig.logLevel = logLevel;
globalState.appController.updateClashConfigDebounce();
Widget _buildAppSection() {
final items = [
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined),
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet),
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.systemProxy = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
return ListItem.switchItem(
leading: const Icon(Icons.expand_outlined),
title: Text(appLocalizations.compatible),
subtitle: Text(appLocalizations.compatibleDesc),
delegate: SwitchDelegate(
value: isCompatible,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCompatible = value;
await appController.updateClashConfig(isPatch: false);
await appController.updateGroups();
appController.changeProxy();
},
),
);
},
),
];
return Section(
title: appLocalizations.app,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
);
}
@override
Widget build(BuildContext context) {
List<Widget> items = [
_showLogLevelDialog(LogLevel value) {
globalState.showCommonDialog(
child: AlertDialog(
title: Text(appLocalizations.logLevel),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
for (final logLevel in LogLevel.values)
ListItem.radio(
delegate: RadioDelegate<LogLevel>(
value: logLevel,
groupValue: value,
onChanged: (LogLevel? value) {
if (value == null) {
return;
}
final appController = globalState.appController;
appController.clashConfig.logLevel = value;
appController.updateClashConfigDebounce();
Navigator.of(context).pop();
},
),
title: Text(logLevel.name),
)
],
),
),
),
);
}
Widget _buildGeneralSection() {
final items = [
Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
onTab: () {
_showLogLevelDialog(value);
},
);
},
),
Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
@@ -54,9 +172,9 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onTab: () {
_modifyMixedPort(mixedPort);
},
padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 4),
leading: const Icon(Icons.adjust),
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text(appLocalizations.proxyPortDesc),
trailing: FilledButton.tonal(
onPressed: () {
_modifyMixedPort(mixedPort);
@@ -68,6 +186,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,92 +222,175 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
// 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();
// },
// ),
// );
// },
// ),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.unifiedDelay,
builder: (_, unifiedDelay, __) {
return ListItem.switchItem(
leading: const Icon(Icons.expand),
title: Text(appLocalizations.compatible),
subtitle: Text(appLocalizations.compatibleDesc),
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: isCompatible,
value: unifiedDelay,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCompatible = value;
await appController.updateClashConfig(isPatch: false);
await appController.updateGroups();
appController.changeProxy();
appController.clashConfig.unifiedDelay = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Padding(
padding: kMaterialListPadding,
child: Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.feedback),
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)
DropdownMenuEntry<LogLevel>(
value: logLevel,
label: logLevel.name,
)
],
onSelected: _updateLoglevel,
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) {
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
builder: (_, tcpConcurrent, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.geodataLoader == geodataLoaderMemconservative,
builder: (_, memconservative, __) {
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: memconservative,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative
: geodataLoaderStandard;
appController.updateClashConfigDebounce();
},
),
);
},
),
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 : '';
appController.updateClashConfigDebounce();
},
),
);
},
),
];
return Section(
title: appLocalizations.general,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
);
}
Widget _buildMoreSection() {
final items = [
if (false)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
return ListItem.switchItem(
leading: const Icon(
Icons.important_devices_outlined
),
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();
},
),
);
},
),
),
];
return ListView.separated(
if(items.isEmpty) return Container();
return Section(
title: appLocalizations.general,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
);
}
@override
Widget build(BuildContext context) {
List<Widget> items = [
_buildAppSection(),
_buildGeneralSection(),
_buildMoreSection(),
];
return ListView.builder(
padding: const EdgeInsets.only(bottom: 32),
itemBuilder: (_, index) {
return Container(
alignment: Alignment.center,
child: items[index],
);
},
separatorBuilder: (_, __) {
return const Divider(
height: 0,
);
},
itemCount: items.length,
);
}

View File

@@ -1,140 +1,140 @@
import 'dart:async';
import 'package:fl_clash/clash/core.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
class ConnectionsFragment extends StatefulWidget {
const ConnectionsFragment({super.key});
@override
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends State<ConnectionsFragment> {
final connectionsNotifier = ValueNotifier<List<Connection>>([]);
Map<String, String?> idPackageNameMap = {};
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_getConnections();
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (mounted) {
_getConnections();
}
});
});
}
_getConnections() {
connectionsNotifier.value = clashCore
.getConnections();
}
@override
void dispose() {
super.dispose();
timer?.cancel();
timer = null;
}
Future<ImageProvider?> _getPackageIconWithConnection(
Connection connection) async {
final uid = connection.metadata.uid;
// if(globalState.packageNameMap[uid] == null){
// globalState.packageNameMap[uid] = await app?.getPackageName(connection.metadata);
// }
final packageName = globalState.packageNameMap[uid];
if(packageName == null) return null;
return await app?.getPackageIcon(packageName);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<Connection>>(
valueListenable: connectionsNotifier,
builder: (_, List<Connection> connections, __) {
if (connections.isEmpty) {
return const NullStatus(
label: "未开启代理,或者没有连接数据",
);
}
return ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (_, index) {
final connection = connections[index];
return ListTile(
titleAlignment: ListTileTitleAlignment.top,
leading: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIconWithConnection(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(connection.metadata.host.isNotEmpty
? connection.metadata.host
: connection.metadata.destinationIP),
Padding(
padding: const EdgeInsets.only(
top: 12,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
),
],
),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
);
},
);
}
}
// import 'dart:async';
//
// import 'package:fl_clash/clash/core.dart';
// import 'package:fl_clash/models/models.dart';
// import 'package:fl_clash/plugins/app.dart';
// import 'package:fl_clash/state.dart';
// import 'package:fl_clash/widgets/widgets.dart';
// import 'package:flutter/material.dart';
//
// class ConnectionsFragment extends StatefulWidget {
// const ConnectionsFragment({super.key});
//
// @override
// State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
// }
//
// class _ConnectionsFragmentState extends State<ConnectionsFragment> {
// final connectionsNotifier = ValueNotifier<List<Connection>>([]);
// Map<String, String?> idPackageNameMap = {};
//
// Timer? timer;
//
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// _getConnections();
// if (timer != null) {
// timer?.cancel();
// timer = null;
// }
// timer = Timer.periodic(const Duration(seconds: 3), (timer) {
// if (mounted) {
// _getConnections();
// }
// });
// });
// }
//
// _getConnections() {
// connectionsNotifier.value = clashCore
// .getConnections();
// }
//
// @override
// void dispose() {
// super.dispose();
// timer?.cancel();
// timer = null;
// }
//
// Future<ImageProvider?> _getPackageIconWithConnection(
// Connection connection) async {
// final uid = connection.metadata.uid;
// // if(globalState.packageNameMap[uid] == null){
// // globalState.packageNameMap[uid] = await app?.getPackageName(connection.metadata);
// // }
// final packageName = globalState.packageNameMap[uid];
// if(packageName == null) return null;
// return await app?.getPackageIcon(packageName);
// }
//
// @override
// Widget build(BuildContext context) {
// return ValueListenableBuilder<List<Connection>>(
// valueListenable: connectionsNotifier,
// builder: (_, List<Connection> connections, __) {
// if (connections.isEmpty) {
// return const NullStatus(
// label: "未开启代理,或者没有连接数据",
// );
// }
// return ListView.separated(
// physics: const AlwaysScrollableScrollPhysics(),
// itemBuilder: (_, index) {
// final connection = connections[index];
// return ListTile(
// titleAlignment: ListTileTitleAlignment.top,
// leading: Container(
// margin: const EdgeInsets.only(top: 4),
// width: 48,
// height: 48,
// child: FutureBuilder<ImageProvider?>(
// future: _getPackageIconWithConnection(connection),
// builder: (_, snapshot) {
// if (!snapshot.hasData && snapshot.data == null) {
// return Container();
// } else {
// return Image(
// image: snapshot.data!,
// gaplessPlayback: true,
// width: 48,
// height: 48,
// );
// }
// },
// ),
// ),
// contentPadding:
// const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
// title: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(connection.metadata.host.isNotEmpty
// ? connection.metadata.host
// : connection.metadata.destinationIP),
// Padding(
// padding: const EdgeInsets.only(
// top: 12,
// ),
// child: Wrap(
// runSpacing: 8,
// spacing: 8,
// children: [
// for (final chain in connection.chains)
// CommonChip(
// label: chain,
// ),
// ],
// ),
// ),
// ],
// ),
// trailing: IconButton(
// icon: const Icon(Icons.block),
// onPressed: () {},
// ),
// );
// },
// separatorBuilder: (BuildContext context, int index) {
// return const Divider(
// height: 0,
// );
// },
// itemCount: connections.length,
// );
// },
// );
// }
// }

View File

@@ -25,6 +25,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
bool isStart,
) async {
if (!isInit) return;
timeoutNotifier.value = false;
if (_preIsStart == false && _preIsStart == isStart) return;
if (cancelToken != null) {
cancelToken!.cancel();
@@ -151,9 +152,10 @@ class _NetworkDetectionState extends State<NetworkDetection> {
builder: (_, timeout, __) {
if (timeout) {
return Text(
appLocalizations.ipCheckTimeout,
style: context.textTheme.titleLarge
?.toSoftBold(),
"timeout",
style: context.textTheme.titleMedium
?.copyWith(color: Colors.red)
.toSoftBold(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);

View File

@@ -21,26 +21,17 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
.asMap()
.map(
(index, e) => MapEntry(
index,
Point(
(index + initPoints.length).toDouble(),
e.speed.toDouble(),
),
),
)
index,
Point(
(index + initPoints.length).toDouble(),
e.speed.toDouble(),
),
),
)
.values
.toList();
var pointsRaw = [...initPoints, ...trafficPoints];
List<Point> points;
if (pointsRaw.length > 60) {
points = pointsRaw
.getRange(pointsRaw.length - 61, pointsRaw.length - 1)
.toList();
} else {
points = pointsRaw;
}
return points;
return [...initPoints, ...trafficPoints];
}
Traffic _getLastTraffic(List<Traffic> traffics) {
@@ -53,11 +44,10 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
required IconData iconData,
required TrafficValue value,
}) {
final showValue = value.showValue;
final showUnit = "${value.showUnit}/s";
final titleLargeSoftBold =
Theme.of(context).textTheme.titleLarge?.toSoftBold();
Theme.of(context).textTheme.titleLarge?.toSoftBold();
final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight();
final valueText = Text(
showValue,
@@ -121,7 +111,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
@override
Widget build(BuildContext context) {
return CommonCard(
info: Info(
info: Info(
label: appLocalizations.networkSpeed,
iconData: Icons.speed,
),
@@ -172,4 +162,4 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
),
);
}
}
}

View File

@@ -55,21 +55,11 @@ class TrafficUsage extends StatelessWidget {
label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off,
),
child: Selector<AppState, List<Traffic>>(
selector: (_, appState) => appState.traffics,
builder: (_, traffics, __) {
final trafficTotal = traffics.isNotEmpty
? traffics.reduce(
(value, element) {
return Traffic(
up: element.up.value + value.up.value,
down: element.down.value + value.down.value,
);
},
)
: Traffic();
final upTrafficValue = trafficTotal.up;
final downTrafficValue = trafficTotal.down;
child: Selector<AppState, Traffic>(
selector: (_, appState) => appState.totalTraffic,
builder: (_, totalTraffic, __) {
final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down;
return Padding(
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
@@ -80,7 +70,7 @@ class TrafficUsage extends StatelessWidget {
child: getTrafficDataItem(
context,
Icons.arrow_upward,
upTrafficValue,
upTotalTrafficValue,
),
),
const SizedBox(
@@ -91,7 +81,7 @@ class TrafficUsage extends StatelessWidget {
child: getTrafficDataItem(
context,
Icons.arrow_downward,
downTrafficValue,
downTotalTrafficValue,
),
),
],

View File

@@ -9,4 +9,5 @@ export 'config.dart';
export 'application_setting.dart';
export 'about.dart';
export 'backup_and_recovery.dart';
export 'resources.dart';
export 'resources.dart';
export 'requests.dart';

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
@@ -22,14 +23,21 @@ class _LogsFragmentState extends State<LogsFragment> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
logsNotifier.value = context.read<AppState>().logs;
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
logsNotifier.value = List<Log>.from(appState.logs);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
logsNotifier.value = globalState.appController.appState.logs;
final logs = List<Log>.from(appState.logs);
if (!const ListEquality<Log>().equals(
logsNotifier.value,
logs,
)) {
logsNotifier.value = logs;
}
});
});
}

View File

@@ -152,6 +152,7 @@ class _EditProfileState extends State<EditProfile> {
vertical: 16,
),
child: ListView.separated(
primary: true,
itemBuilder: (_, index) {
return items[index];
},

View File

@@ -1,5 +1,6 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/fragments/profiles/view_profile.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
@@ -15,6 +16,7 @@ enum ProfileActions {
edit,
update,
delete,
view,
}
class ProfilesFragment extends StatefulWidget {
@@ -62,7 +64,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
},
);
},
icon: const Icon(Icons.download),
icon: const Icon(Icons.sync),
),
const SizedBox(
width: 8,
@@ -198,6 +200,16 @@ class _ProfileItemState extends State<ProfileItem> {
);
}
_handleViewProfile(Profile profile) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ViewProfile(
profile: profile.copyWith(),
),
),
);
}
_buildTitle(Profile profile) {
final textTheme = context.textTheme;
final userInfo = profile.userInfo ?? UserInfo();
@@ -207,7 +219,7 @@ class _ProfileItemState extends State<ProfileItem> {
final totalShow = TrafficValue(value: total).show;
final progress = total == 0 ? 0.0 : use / total;
final expireShow = userInfo.expire == 0
? "长期有效"
? appLocalizations.infiniteTime
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
return Container(
padding: const EdgeInsets.symmetric(vertical: 4),
@@ -255,7 +267,7 @@ class _ProfileItemState extends State<ProfileItem> {
Row(
children: [
Text(
"到期时间:",
appLocalizations.expirationTime,
style: textTheme.labelMedium?.toLighter(),
),
const SizedBox(
@@ -316,6 +328,11 @@ class _ProfileItemState extends State<ProfileItem> {
label: appLocalizations.delete,
iconData: Icons.delete,
),
// CommonPopupMenuItem(
// action: ProfileActions.view,
// label: "查看",
// iconData: Icons.visibility,
// ),
],
onSelected: (ProfileActions? action) async {
switch (action) {
@@ -328,6 +345,9 @@ class _ProfileItemState extends State<ProfileItem> {
case ProfileActions.update:
_handleUpdateProfile(profile.id);
break;
case ProfileActions.view:
_handleViewProfile(profile);
break;
case null:
break;
}

View File

@@ -0,0 +1,171 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:re_editor/re_editor.dart';
import 'package:re_highlight/languages/yaml.dart';
import 'package:re_highlight/styles/intellij-light.dart';
class ViewProfile extends StatefulWidget {
final Profile profile;
const ViewProfile({
super.key,
required this.profile,
});
@override
State<ViewProfile> createState() => _ViewProfileState();
}
class _ViewProfileState extends State<ViewProfile> {
bool readOnly = true;
final controller = CodeLineEditingController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
final profilePath = await appPath.getProfilePath(widget.profile.id);
if (profilePath == null) {
return;
}
final file = File(profilePath);
final text = await file.readAsString();
controller.text = text;
// _codeController.text = text;
});
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
actions: [
IconButton(
onPressed: controller.undo,
icon: const Icon(Icons.undo),
),
IconButton(
onPressed: controller.redo,
icon: const Icon(Icons.redo),
),
const SizedBox(
width: 8,
)
],
body: CodeEditor(
autofocus: false,
readOnly: readOnly,
scrollbarBuilder: (context, child, details) {
return Scrollbar(
controller: details.controller,
thumbVisibility: true,
interactive: true,
child: child,
);
},
showCursorWhenReadOnly: false,
controller: controller,
toolbarController: const ContextMenuControllerImpl(),
shortcutsActivatorsBuilder:
const DefaultCodeShortcutsActivatorsBuilder(),
indicatorBuilder:
(context, editingController, chunkController, notifier) {
return Row(
children: [
DefaultCodeLineNumber(
controller: editingController,
notifier: notifier,
),
DefaultCodeChunkIndicator(
width: 20,
controller: chunkController,
notifier: notifier,
)
],
);
},
style: CodeEditorStyle(
fontSize: 14,
codeTheme: CodeHighlightTheme(
languages: {
'yaml': CodeHighlightThemeMode(
mode: langYaml,
)
},
theme: intellijLightTheme,
),
),
),
title: "查看",
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
readOnly = !readOnly;
});
},
child: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
),
);
}
}
class ContextMenuItemWidget extends PopupMenuItem<void> {
ContextMenuItemWidget({
super.key,
required String text,
required VoidCallback super.onTap,
}) : super(child: Text(text));
}
class ContextMenuControllerImpl implements SelectionToolbarController {
const ContextMenuControllerImpl();
@override
void hide(BuildContext context) {}
@override
void show({
required BuildContext context,
required CodeLineEditingController controller,
required TextSelectionToolbarAnchors anchors,
Rect? renderRect,
required LayerLink layerLink,
required ValueNotifier<bool> visibility,
}) {
if (controller.selectedText.isEmpty) {
return;
}
showMenu(
context: context,
popUpAnimationStyle: AnimationStyle.noAnimation,
position: RelativeRect.fromSize(
(anchors.secondaryAnchor ?? anchors.primaryAnchor) &
const Size(150, double.infinity),
MediaQuery.of(context).size,
),
items: [
ContextMenuItemWidget(
text: 'Cut',
onTap: () {
controller.cut();
},
),
ContextMenuItemWidget(
text: 'Copy',
onTap: () {
controller.copy();
},
),
ContextMenuItemWidget(
text: 'Paste',
onTap: () {
controller.paste();
},
),
],
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
@@ -59,6 +60,23 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
});
}
_handleTabControllerChange() {
final indexIsChanging = _tabController?.indexIsChanging ?? false;
if (indexIsChanging) return;
final index = _tabController?.index;
if (index == null) return;
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
if (currentGroups.length > index) {
appController.config.updateCurrentGroupName(currentGroups[index].name);
}
}
@override
void dispose() {
super.dispose();
_tabController?.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
@@ -69,26 +87,34 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
}
return child!;
},
child: Selector3<AppState, Config, ClashConfig, ProxiesSelectorState>(
selector: (_, appState, config, clashConfig) {
child: Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
shouldRebuild: (prev, next) {
if (prev.groupNames.length != next.groupNames.length) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
_tabController?.removeListener(_handleTabControllerChange);
_tabController?.dispose();
_tabController = null;
return true;
}
return prev != next;
return false;
},
builder: (_, state, __) {
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_tabController ??= TabController(
length: state.groupNames.length,
initialIndex: index == -1 ? 0 : index,
vsync: this,
);
)..addListener(_handleTabControllerChange);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -99,7 +125,8 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
overlayColor:
const WidgetStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in state.groupNames)
Tab(
@@ -200,7 +227,8 @@ 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;
final proxyName =
appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay(
Delay(
name: proxyName,

178
lib/fragments/requests.dart Normal file
View File

@@ -0,0 +1,178 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
class RequestFragment extends StatefulWidget {
const RequestFragment({super.key});
@override
State<RequestFragment> createState() => _RequestFragmentState();
}
class _RequestFragmentState extends State<RequestFragment> {
final requestsNotifier = ValueNotifier<List<Connection>>([]);
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
requestsNotifier.value = List<Connection>.from(appState.requests);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final requests = List<Connection>.from(appState.requests);
if (!const ListEquality<Connection>().equals(
requestsNotifier.value,
requests,
)) {
requestsNotifier.value = requests;
}
});
});
}
@override
void dispose() {
super.dispose();
timer?.cancel();
_scrollController.dispose();
timer = null;
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<Connection>>(
valueListenable: requestsNotifier,
builder: (_, List<Connection> connections, __) {
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
connections = connections.reversed.toList();
return ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return RequestItem(
key: Key(connection.id),
connection: connection,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
);
},
);
}
}
class RequestItem extends StatelessWidget {
final Connection connection;
const RequestItem({
super.key,
required this.connection,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getRequestText(Metadata metadata) {
var text = "${metadata.network}:://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
return ListTile(
titleAlignment: ListTileTitleAlignment.top,
leading: Platform.isAndroid
? Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
)
: null,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 12,
),
Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
),
],
),
const SizedBox(
height: 12,
),
],
),
);
}
}

View File

@@ -111,9 +111,10 @@ class ThemeFragment extends StatelessWidget {
null,
defaultPrimaryColor,
Colors.pinkAccent,
Colors.lightBlue,
Colors.greenAccent,
Colors.yellowAccent,
Colors.purple
Colors.purple,
];
return Column(
children: [

View File

@@ -166,6 +166,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
delegate: OpenDelegate(
title: appLocalizations.override,
widget: const ConfigFragment(),
extendPageWidth: 360,
),
),
ListItem.open(

View File

@@ -111,6 +111,7 @@
"noMoreInfoDesc": "No more info",
"profileParseErrorDesc": "profile parse error",
"proxyPort": "ProxyPort",
"proxyPortDesc": "Set the clash listening port",
"port": "Port",
"logLevel": "LogLevel",
"show": "Show",
@@ -161,5 +162,27 @@
"country": "Country",
"checkError": "Check error",
"ipCheckTimeout": "Ip check timeout",
"search": "Search"
"search": "Search",
"allowBypass": "Allow applications to bypass VPN",
"allowBypassDesc": "Some apps can bypass VPN when turned on",
"externalController": "ExternalController",
"externalControllerDesc": "Once enabled, the clash kernel can be controlled on port 9090",
"ipv6Desc": "When turned on it will be able to receive ipv6 traffic",
"app": "App",
"general": "General",
"systemProxyDesc": "Attach HTTP proxy to VpnService",
"unifiedDelay": "Unified delay",
"unifiedDelayDesc": "Remove extra delays such as handshaking",
"tcpConcurrent": "Tcp concurrent",
"tcpConcurrentDesc": "Enabling it will allow tcp concurrency",
"geodataLoader": "Geo Low Memory Mode",
"geodataLoaderDesc": "Enabling will use the Geo low memory loader",
"requests": "Requests",
"requestsDesc": "View recently requested data",
"nullRequestsDesc": "No proxy or no request",
"findProcessMode": "Find process",
"findProcessModeDesc": "There is a risk of flashback after opening",
"init": "Init",
"infiniteTime": "Long term effective",
"expirationTime": "Expiration time"
}

View File

@@ -111,6 +111,7 @@
"noMoreInfoDesc": "暂无更多信息",
"profileParseErrorDesc": "配置文件解析错误",
"proxyPort": "代理端口",
"proxyPortDesc": "设置clash监听端口",
"port": "端口",
"logLevel": "日志等级",
"show": "显示",
@@ -161,5 +162,27 @@
"country": "区域",
"checkError": "检测失败",
"ipCheckTimeout": "Ip检测超时",
"search": "搜索"
"search": "搜索",
"allowBypass": "允许应用绕过vpn",
"allowBypassDesc": "开启后部分应用可绕过VPN",
"externalController": "外部控制器",
"externalControllerDesc": "开启后将可以通过9090端口控制clash内核",
"ipv6Desc": "开启后将可以接收ipv6流量",
"app": "应用",
"general": "基础",
"systemProxyDesc": "为VpnService附加HTTP代理",
"unifiedDelay": "统一延迟",
"unifiedDelayDesc": "去除握手等额外延迟",
"tcpConcurrent": "TCP并发",
"tcpConcurrentDesc": "开启后允许tcp并发",
"geodataLoader": "Geo低内存模式",
"geodataLoaderDesc": "开启将使用Geo低内存加载器",
"requests": "请求",
"requestsDesc": "查看最近请求数据",
"nullRequestsDesc": "未开启代理或者没有请求",
"findProcessMode": "查找进程",
"findProcessModeDesc": "开启后存在闪退风险",
"init": "初始化",
"infiniteTime": "长期有效",
"expirationTime": "到期时间"
}

View File

@@ -40,9 +40,14 @@ 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(
"Some apps can bypass VPN when turned on"),
"allowLan": MessageLookupByLibrary.simpleMessage("AllowLan"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage(
"Allow access proxy through the LAN"),
"app": MessageLookupByLibrary.simpleMessage("App"),
"appAccessControl":
MessageLookupByLibrary.simpleMessage("App access control"),
"application": MessageLookupByLibrary.simpleMessage("Application"),
@@ -112,6 +117,12 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
"expirationTime":
MessageLookupByLibrary.simpleMessage("Expiration time"),
"externalController":
MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"Once enabled, the clash kernel can be controlled on port 9090"),
"externalResources":
MessageLookupByLibrary.simpleMessage("External resources"),
"file": MessageLookupByLibrary.simpleMessage("File"),
@@ -119,14 +130,27 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Directly upload profile"),
"filterSystemApp":
MessageLookupByLibrary.simpleMessage("Filter system app"),
"findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"),
"findProcessModeDesc": MessageLookupByLibrary.simpleMessage(
"There is a risk of flashback after opening"),
"general": MessageLookupByLibrary.simpleMessage("General"),
"geoData": MessageLookupByLibrary.simpleMessage("GeoData"),
"geodataLoader":
MessageLookupByLibrary.simpleMessage("Geo Low Memory Mode"),
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage(
"Enabling will use the Geo low memory loader"),
"global": MessageLookupByLibrary.simpleMessage("Global"),
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
"importFromURL":
MessageLookupByLibrary.simpleMessage("Import from URL"),
"infiniteTime":
MessageLookupByLibrary.simpleMessage("Long term effective"),
"init": MessageLookupByLibrary.simpleMessage("Init"),
"ipCheckTimeout":
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
"When turned on it will be able to receive ipv6 traffic"),
"just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"light": MessageLookupByLibrary.simpleMessage("Light"),
@@ -160,6 +184,8 @@ class MessageLookup extends MessageLookupByLibrary {
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("No logs"),
"nullProfileDesc": MessageLookupByLibrary.simpleMessage(
"No profile, Please add a profile"),
"nullRequestsDesc":
MessageLookupByLibrary.simpleMessage("No proxy or no request"),
"other": MessageLookupByLibrary.simpleMessage("Other"),
"outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"),
"override": MessageLookupByLibrary.simpleMessage("Override"),
@@ -195,6 +221,8 @@ class MessageLookup extends MessageLookupByLibrary {
"project": MessageLookupByLibrary.simpleMessage("Project"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage(
"Set the clash listening port"),
"qrcode": MessageLookupByLibrary.simpleMessage("QR code"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage(
"Scan QR code to obtain profile"),
@@ -207,6 +235,9 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Only recovery profiles"),
"recoverySuccess":
MessageLookupByLibrary.simpleMessage("Recovery success"),
"requests": MessageLookupByLibrary.simpleMessage("Requests"),
"requestsDesc": MessageLookupByLibrary.simpleMessage(
"View recently requested data"),
"resources": MessageLookupByLibrary.simpleMessage("Resources"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
"External resource related info"),
@@ -224,9 +255,14 @@ class MessageLookup extends MessageLookupByLibrary {
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("Tab animation"),
"tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"When enabled, the home tab will add a toggle animation"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("Tcp concurrent"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage(
"Enabling it will allow tcp concurrency"),
"theme": MessageLookupByLibrary.simpleMessage("Theme"),
"themeColor": MessageLookupByLibrary.simpleMessage("Theme color"),
"themeDesc": MessageLookupByLibrary.simpleMessage(
@@ -241,6 +277,9 @@ class MessageLookup extends MessageLookupByLibrary {
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage(
"unable to update current profile"),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("Unified delay"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage(
"Remove extra delays such as handshaking"),
"unknown": MessageLookupByLibrary.simpleMessage("Unknown"),
"update": MessageLookupByLibrary.simpleMessage("Update"),
"upload": MessageLookupByLibrary.simpleMessage("Upload"),

View File

@@ -36,8 +36,12 @@ 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("允许通过局域网访问代理"),
"app": MessageLookupByLibrary.simpleMessage("应用"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
"application": MessageLookupByLibrary.simpleMessage("应用程序"),
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
@@ -93,16 +97,30 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制clash内核"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
"findProcessModeDesc":
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"general": MessageLookupByLibrary.simpleMessage("基础"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
"geodataLoaderDesc":
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
"global": MessageLookupByLibrary.simpleMessage("全局"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hours": MessageLookupByLibrary.simpleMessage("小时"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"),
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
@@ -131,6 +149,7 @@ class MessageLookup extends MessageLookupByLibrary {
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
"nullProfileDesc":
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("未开启代理或者没有请求"),
"other": MessageLookupByLibrary.simpleMessage("其他"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"override": MessageLookupByLibrary.simpleMessage("覆写"),
@@ -160,6 +179,7 @@ class MessageLookup extends MessageLookupByLibrary {
"project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置clash监听端口"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"),
@@ -167,6 +187,8 @@ class MessageLookup extends MessageLookupByLibrary {
"recoveryDesc": MessageLookupByLibrary.simpleMessage("从WebDAV恢复数据"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"requests": MessageLookupByLibrary.simpleMessage("请求"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求数据"),
"resources": MessageLookupByLibrary.simpleMessage("资源"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
@@ -182,9 +204,13 @@ class MessageLookup extends MessageLookupByLibrary {
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"tabAnimationDesc":
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许tcp并发"),
"theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
@@ -196,6 +222,8 @@ class MessageLookup extends MessageLookupByLibrary {
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("上传"),

View File

@@ -1170,6 +1170,16 @@ class AppLocalizations {
);
}
/// `Set the clash listening port`
String get proxyPortDesc {
return Intl.message(
'Set the clash listening port',
name: 'proxyPortDesc',
desc: '',
args: [],
);
}
/// `Port`
String get port {
return Intl.message(
@@ -1679,6 +1689,226 @@ class AppLocalizations {
args: [],
);
}
/// `Allow applications to bypass VPN`
String get allowBypass {
return Intl.message(
'Allow applications to bypass VPN',
name: 'allowBypass',
desc: '',
args: [],
);
}
/// `Some apps can bypass VPN when turned on`
String get allowBypassDesc {
return Intl.message(
'Some apps can bypass VPN when turned on',
name: 'allowBypassDesc',
desc: '',
args: [],
);
}
/// `ExternalController`
String get externalController {
return Intl.message(
'ExternalController',
name: 'externalController',
desc: '',
args: [],
);
}
/// `Once enabled, the clash kernel can be controlled on port 9090`
String get externalControllerDesc {
return Intl.message(
'Once enabled, the clash kernel can be controlled on port 9090',
name: 'externalControllerDesc',
desc: '',
args: [],
);
}
/// `When turned on it will be able to receive ipv6 traffic`
String get ipv6Desc {
return Intl.message(
'When turned on it will be able to receive ipv6 traffic',
name: 'ipv6Desc',
desc: '',
args: [],
);
}
/// `App`
String get app {
return Intl.message(
'App',
name: 'app',
desc: '',
args: [],
);
}
/// `General`
String get general {
return Intl.message(
'General',
name: 'general',
desc: '',
args: [],
);
}
/// `Attach HTTP proxy to VpnService`
String get systemProxyDesc {
return Intl.message(
'Attach HTTP proxy to VpnService',
name: 'systemProxyDesc',
desc: '',
args: [],
);
}
/// `Unified delay`
String get unifiedDelay {
return Intl.message(
'Unified delay',
name: 'unifiedDelay',
desc: '',
args: [],
);
}
/// `Remove extra delays such as handshaking`
String get unifiedDelayDesc {
return Intl.message(
'Remove extra delays such as handshaking',
name: 'unifiedDelayDesc',
desc: '',
args: [],
);
}
/// `Tcp concurrent`
String get tcpConcurrent {
return Intl.message(
'Tcp concurrent',
name: 'tcpConcurrent',
desc: '',
args: [],
);
}
/// `Enabling it will allow tcp concurrency`
String get tcpConcurrentDesc {
return Intl.message(
'Enabling it will allow tcp concurrency',
name: 'tcpConcurrentDesc',
desc: '',
args: [],
);
}
/// `Geo Low Memory Mode`
String get geodataLoader {
return Intl.message(
'Geo Low Memory Mode',
name: 'geodataLoader',
desc: '',
args: [],
);
}
/// `Enabling will use the Geo low memory loader`
String get geodataLoaderDesc {
return Intl.message(
'Enabling will use the Geo low memory loader',
name: 'geodataLoaderDesc',
desc: '',
args: [],
);
}
/// `Requests`
String get requests {
return Intl.message(
'Requests',
name: 'requests',
desc: '',
args: [],
);
}
/// `View recently requested data`
String get requestsDesc {
return Intl.message(
'View recently requested data',
name: 'requestsDesc',
desc: '',
args: [],
);
}
/// `No proxy or no request`
String get nullRequestsDesc {
return Intl.message(
'No proxy or no request',
name: 'nullRequestsDesc',
desc: '',
args: [],
);
}
/// `Find process`
String get findProcessMode {
return Intl.message(
'Find process',
name: 'findProcessMode',
desc: '',
args: [],
);
}
/// `There is a risk of flashback after opening`
String get findProcessModeDesc {
return Intl.message(
'There is a risk of flashback after opening',
name: 'findProcessModeDesc',
desc: '',
args: [],
);
}
/// `Init`
String get init {
return Intl.message(
'Init',
name: 'init',
desc: '',
args: [],
);
}
/// `Long term effective`
String get infiniteTime {
return Intl.message(
'Long term effective',
name: 'infiniteTime',
desc: '',
args: [],
);
}
/// `Expiration time`
String get expirationTime {
return Intl.message(
'Expiration time',
name: 'expirationTime',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -22,12 +22,11 @@ Future<void> main() async {
isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
);
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
appState.navigationItems = navigation.getItems(
openLogs: config.openLogs,
hasProxies: false,
);
globalState.updateNavigationItems(
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
@@ -55,12 +54,19 @@ Future<void> vpnService() async {
int.parse(fd),
);
}));
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
final appLocalizations = await AppLocalizations.load(
other.getLocaleForString(config.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
@@ -69,6 +75,7 @@ Future<void> vpnService() async {
handleStart() async {
await app?.tip(appLocalizations.startVpn);
await globalState.startSystemProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
@@ -80,19 +87,18 @@ Future<void> vpnService() async {
];
}
if (appState.isInit) {
handleStart();
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.stopSystemProxy();
clashCore.shutdown();
exit(0);
},
),
);
}
handleStart();
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.stopSystemProxy();
clashCore.shutdown();
exit(0);
},
),
);
}
class ClashMessageListenerWithVpn with ClashMessageListener {

View File

@@ -1,7 +1,10 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'connection.dart';
import 'ffi.dart';
import 'log.dart';
import 'navigation.dart';
@@ -19,6 +22,7 @@ class AppState with ChangeNotifier {
bool _isInit;
VersionInfo? _versionInfo;
List<Traffic> _traffics;
Traffic _totalTraffic;
List<Log> _logs;
String _currentLabel;
SystemColorSchemes _systemColorSchemes;
@@ -29,6 +33,7 @@ class AppState with ChangeNotifier {
bool _isCompatible;
List<Group> _groups;
double _viewWidth;
List<Connection> _requests;
AppState({
required Mode mode,
@@ -42,7 +47,9 @@ class AppState with ChangeNotifier {
_viewWidth = 0,
_selectedMap = selectedMap,
_sortNum = 0,
_requests = [],
_mode = mode,
_totalTraffic = Traffic(),
_delayMap = {},
_groups = [],
_isCompatible = isCompatible,
@@ -152,8 +159,39 @@ class AppState with ChangeNotifier {
}
}
addTraffic(Traffic value) {
_traffics = List.from(_traffics)..add(value);
addTraffic(Traffic traffic) {
_traffics = List.from(_traffics)..add(traffic);
const maxLength = 60;
if (_traffics.length > maxLength) {
_traffics = _traffics.sublist(_traffics.length - maxLength);
}
notifyListeners();
}
Traffic get totalTraffic => _totalTraffic;
set totalTraffic(Traffic value) {
if (_totalTraffic != value) {
_totalTraffic = value;
notifyListeners();
}
}
List<Connection> get requests => _requests;
set requests(List<Connection> value) {
if (_requests != value) {
_requests = value;
notifyListeners();
}
}
addRequest(Connection value) {
_requests.add(value);
final maxLength = Platform.isAndroid ? 1000 : 60;
if (_requests.length > maxLength) {
_requests = _requests.sublist(_requests.length - maxLength);
}
notifyListeners();
}
@@ -168,8 +206,9 @@ class AppState with ChangeNotifier {
addLog(Log log) {
_logs.add(log);
if (_logs.length > 60) {
_logs = _logs.sublist(_logs.length - 60);
final maxLength = Platform.isAndroid ? 1000 : 60;
if (_logs.length > maxLength) {
_logs = _logs.sublist(_logs.length - maxLength);
}
notifyListeners();
}

View File

@@ -108,29 +108,34 @@ class Dns {
class ClashConfig extends ChangeNotifier {
int _mixedPort;
bool _allowLan;
Mode _mode;
bool _ipv6;
String _geodataLoader;
LogLevel _logLevel;
String _externalController;
Mode _mode;
FindProcessMode _findProcessMode;
bool _unifiedDelay;
bool _tcpConcurrent;
Tun _tun;
Dns _dns;
List<String> _rules;
ClashConfig({
int? mixedPort,
Mode? mode,
bool? allowLan,
LogLevel? logLevel,
Tun? tun,
Dns? dns,
List<String>? rules,
}) : _mixedPort = mixedPort ?? 7890,
_mode = mode ?? Mode.rule,
_allowLan = allowLan ?? false,
_logLevel = logLevel ?? LogLevel.info,
_tun = tun ?? const Tun(),
_dns = dns ?? Dns(),
_rules = rules ?? [];
ClashConfig()
: _mixedPort = 7890,
_mode = Mode.rule,
_ipv6 = false,
_findProcessMode = FindProcessMode.off,
_allowLan = false,
_tcpConcurrent = false,
_logLevel = LogLevel.info,
_tun = const Tun(),
_unifiedDelay = false,
_geodataLoader = geodataLoaderMemconservative,
_externalController = '',
_dns = Dns(),
_rules = [];
@JsonKey(name: "mixed-port")
@JsonKey(name: "mixed-port", defaultValue: 7890)
int get mixedPort => _mixedPort;
set mixedPort(int value) {
@@ -140,6 +145,7 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(defaultValue: Mode.rule)
Mode get mode => _mode;
set mode(Mode value) {
@@ -149,6 +155,16 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(name: "find-process-mode", defaultValue: FindProcessMode.off)
FindProcessMode get findProcessMode => _findProcessMode;
set findProcessMode(FindProcessMode value) {
if (_findProcessMode != value) {
_findProcessMode = value;
notifyListeners();
}
}
@JsonKey(name: "allow-lan")
bool get allowLan => _allowLan;
@@ -159,7 +175,7 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(name: "log-level")
@JsonKey(name: "log-level", defaultValue: LogLevel.info)
LogLevel get logLevel => _logLevel;
set logLevel(LogLevel value) {
@@ -169,6 +185,56 @@ 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();
}
}
@JsonKey(name: "geodata-loader", defaultValue: geodataLoaderMemconservative)
String get geodataLoader => _geodataLoader;
set geodataLoader(String value) {
if (_geodataLoader != value) {
_geodataLoader = value;
notifyListeners();
}
}
@JsonKey(name: "unified-delay", defaultValue: false)
bool get unifiedDelay => _unifiedDelay;
set unifiedDelay(bool value) {
if (_unifiedDelay != value) {
_unifiedDelay = value;
notifyListeners();
}
}
@JsonKey(name: "tcp-concurrent", defaultValue: false)
bool get tcpConcurrent => _tcpConcurrent;
set tcpConcurrent(bool value) {
if (_tcpConcurrent != value) {
_tcpConcurrent = value;
notifyListeners();
}
}
Tun get tun => _tun;
set tun(Tun value) {
@@ -217,17 +283,6 @@ class ClashConfig extends ChangeNotifier {
return _$ClashConfigFromJson(json);
}
ClashConfig copyWith({Tun? tun}) {
return ClashConfig(
mixedPort: mixedPort,
mode: mode,
logLevel: logLevel,
tun: tun ?? this.tun,
dns: dns,
allowLan: allowLan,
);
}
@override
String toString() {
return 'ClashConfig{_mixedPort: $_mixedPort, _allowLan: $_allowLan, _mode: $_mode, _logLevel: $_logLevel, _tun: $_tun, _dns: $_dns, _rules: $_rules}';

View File

@@ -24,6 +24,17 @@ class AccessControl with _$AccessControl {
_$AccessControlFromJson(json);
}
@freezed
class Props with _$Props {
const factory Props({
AccessControl? accessControl,
bool? allowBypass,
bool? systemProxy,
}) = _Props;
factory Props.fromJson(Map<String, Object?> json) => _$PropsFromJson(json);
}
@JsonSerializable()
class Config extends ChangeNotifier {
List<Profile> _profiles;
@@ -42,6 +53,8 @@ class Config extends ChangeNotifier {
AccessControl _accessControl;
bool _isAnimateToPage;
bool _autoCheckUpdate;
bool _allowBypass;
bool _systemProxy;
DAV? _dav;
Config()
@@ -57,8 +70,10 @@ class Config extends ChangeNotifier {
_isMinimizeOnExit = true,
_isAccessControl = false,
_autoCheckUpdate = true,
_systemProxy = true,
_accessControl = const AccessControl(),
_isAnimateToPage = true;
_isAnimateToPage = true,
_allowBypass = true;
deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList();
@@ -135,6 +150,15 @@ class Config extends ChangeNotifier {
}
}
String? get currentGroupName => currentProfile?.currentGroupName;
updateCurrentGroupName(String groupName) {
if (currentProfile?.currentGroupName != groupName) {
currentProfile?.currentGroupName = groupName;
notifyListeners();
}
}
SelectedMap get currentSelectedMap {
return currentProfile?.selectedMap ?? {};
}
@@ -306,8 +330,34 @@ 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();
}
}
@JsonKey(defaultValue: true)
bool get systemProxy {
return _systemProxy;
}
set systemProxy(bool value) {
if (_systemProxy != value) {
_systemProxy = value;
notifyListeners();
}
}
update([
Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all,
]) {
if (config != null) {
_profiles = config._profiles;
for (final profile in config._profiles) {
@@ -326,6 +376,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

@@ -14,6 +14,7 @@ class Metadata with _$Metadata {
required String destinationIP,
required String destinationPort,
required String host,
required String process,
required String remoteDestination,
}) = _Metadata;

View File

@@ -2,6 +2,7 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/models/connection.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/ffi.g.dart';
@@ -66,16 +67,25 @@ class Now with _$Now {
@freezed
class Process with _$Process {
const factory Process({
required int uid,
required String network,
required String source,
required String target,
required int id,
required Metadata metadata,
}) = _Process;
factory Process.fromJson(Map<String, Object?> json) =>
_$ProcessFromJson(json);
}
@freezed
class ProcessMapItem with _$ProcessMapItem {
const factory ProcessMapItem({
required int id,
required String value,
}) = _ProcessMapItem;
factory ProcessMapItem.fromJson(Map<String, Object?> json) =>
_$ProcessMapItemFromJson(json);
}
@freezed
class ExternalProvider with _$ExternalProvider {
const factory ExternalProvider({

View File

@@ -35,27 +35,36 @@ Map<String, dynamic> _$DnsToJson(Dns instance) => <String, dynamic>{
'fake-ip-filter': instance.fakeIpFilter,
};
ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig(
mixedPort: (json['mixed-port'] as num?)?.toInt(),
mode: $enumDecodeNullable(_$ModeEnumMap, json['mode']),
allowLan: json['allow-lan'] as bool?,
logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']),
tun: json['tun'] == null
? null
: Tun.fromJson(json['tun'] as Map<String, dynamic>),
dns: json['dns'] == null
? null
: Dns.fromJson(json['dns'] as Map<String, dynamic>),
rules:
(json['rules'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
..mixedPort = (json['mixed-port'] as num?)?.toInt() ?? 7890
..mode = $enumDecodeNullable(_$ModeEnumMap, json['mode']) ?? Mode.rule
..findProcessMode = $enumDecodeNullable(
_$FindProcessModeEnumMap, json['find-process-mode']) ??
FindProcessMode.off
..allowLan = json['allow-lan'] as bool
..logLevel =
$enumDecodeNullable(_$LogLevelEnumMap, json['log-level']) ?? LogLevel.info
..externalController = json['external-controller'] as String? ?? ''
..ipv6 = json['ipv6'] as bool? ?? false
..geodataLoader = json['geodata-loader'] as String? ?? 'memconservative'
..unifiedDelay = json['unified-delay'] as bool? ?? false
..tcpConcurrent = json['tcp-concurrent'] as bool? ?? false
..tun = Tun.fromJson(json['tun'] as Map<String, dynamic>)
..dns = Dns.fromJson(json['dns'] as Map<String, dynamic>)
..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList();
Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
<String, dynamic>{
'mixed-port': instance.mixedPort,
'mode': _$ModeEnumMap[instance.mode]!,
'find-process-mode': _$FindProcessModeEnumMap[instance.findProcessMode]!,
'allow-lan': instance.allowLan,
'log-level': _$LogLevelEnumMap[instance.logLevel]!,
'external-controller': instance.externalController,
'ipv6': instance.ipv6,
'geodata-loader': instance.geodataLoader,
'unified-delay': instance.unifiedDelay,
'tcp-concurrent': instance.tcpConcurrent,
'tun': instance.tun,
'dns': instance.dns,
'rules': instance.rules,
@@ -67,6 +76,11 @@ const _$ModeEnumMap = {
Mode.direct: 'direct',
};
const _$FindProcessModeEnumMap = {
FindProcessMode.always: 'always',
FindProcessMode.off: 'off',
};
const _$LogLevelEnumMap = {
LogLevel.debug: 'debug',
LogLevel.info: 'info',

View File

@@ -239,3 +239,193 @@ 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;
bool? get systemProxy => 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, bool? systemProxy});
$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,
Object? systemProxy = 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?,
systemProxy: freezed == systemProxy
? _value.systemProxy
: systemProxy // 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, bool? systemProxy});
@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,
Object? systemProxy = 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?,
systemProxy: freezed == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$PropsImpl implements _Props {
const _$PropsImpl({this.accessControl, this.allowBypass, this.systemProxy});
factory _$PropsImpl.fromJson(Map<String, dynamic> json) =>
_$$PropsImplFromJson(json);
@override
final AccessControl? accessControl;
@override
final bool? allowBypass;
@override
final bool? systemProxy;
@override
String toString() {
return 'Props(accessControl: $accessControl, allowBypass: $allowBypass, systemProxy: $systemProxy)';
}
@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) &&
(identical(other.systemProxy, systemProxy) ||
other.systemProxy == systemProxy));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, accessControl, allowBypass, systemProxy);
@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,
final bool? systemProxy}) = _$PropsImpl;
factory _Props.fromJson(Map<String, dynamic> json) = _$PropsImpl.fromJson;
@override
AccessControl? get accessControl;
@override
bool? get allowBypass;
@override
bool? get systemProxy;
@override
@JsonKey(ignore: true)
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -32,7 +32,9 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
: DAV.fromJson(json['dav'] as Map<String, dynamic>)
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true;
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? true;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -52,6 +54,8 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'isAnimateToPage': instance.isAnimateToPage,
'isCompatible': instance.isCompatible,
'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
};
const _$ThemeModeEnumMap = {
@@ -93,3 +97,19 @@ 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?,
systemProxy: json['systemProxy'] as bool?,
);
Map<String, dynamic> _$$PropsImplToJson(_$PropsImpl instance) =>
<String, dynamic>{
'accessControl': instance.accessControl,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
};

View File

@@ -27,6 +27,7 @@ mixin _$Metadata {
String get destinationIP => throw _privateConstructorUsedError;
String get destinationPort => throw _privateConstructorUsedError;
String get host => throw _privateConstructorUsedError;
String get process => throw _privateConstructorUsedError;
String get remoteDestination => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -48,6 +49,7 @@ abstract class $MetadataCopyWith<$Res> {
String destinationIP,
String destinationPort,
String host,
String process,
String remoteDestination});
}
@@ -71,6 +73,7 @@ class _$MetadataCopyWithImpl<$Res, $Val extends Metadata>
Object? destinationIP = null,
Object? destinationPort = null,
Object? host = null,
Object? process = null,
Object? remoteDestination = null,
}) {
return _then(_value.copyWith(
@@ -102,6 +105,10 @@ class _$MetadataCopyWithImpl<$Res, $Val extends Metadata>
? _value.host
: host // ignore: cast_nullable_to_non_nullable
as String,
process: null == process
? _value.process
: process // ignore: cast_nullable_to_non_nullable
as String,
remoteDestination: null == remoteDestination
? _value.remoteDestination
: remoteDestination // ignore: cast_nullable_to_non_nullable
@@ -126,6 +133,7 @@ abstract class _$$MetadataImplCopyWith<$Res>
String destinationIP,
String destinationPort,
String host,
String process,
String remoteDestination});
}
@@ -147,6 +155,7 @@ class __$$MetadataImplCopyWithImpl<$Res>
Object? destinationIP = null,
Object? destinationPort = null,
Object? host = null,
Object? process = null,
Object? remoteDestination = null,
}) {
return _then(_$MetadataImpl(
@@ -178,6 +187,10 @@ class __$$MetadataImplCopyWithImpl<$Res>
? _value.host
: host // ignore: cast_nullable_to_non_nullable
as String,
process: null == process
? _value.process
: process // ignore: cast_nullable_to_non_nullable
as String,
remoteDestination: null == remoteDestination
? _value.remoteDestination
: remoteDestination // ignore: cast_nullable_to_non_nullable
@@ -197,6 +210,7 @@ class _$MetadataImpl implements _Metadata {
required this.destinationIP,
required this.destinationPort,
required this.host,
required this.process,
required this.remoteDestination});
factory _$MetadataImpl.fromJson(Map<String, dynamic> json) =>
@@ -217,11 +231,13 @@ class _$MetadataImpl implements _Metadata {
@override
final String host;
@override
final String process;
@override
final String remoteDestination;
@override
String toString() {
return 'Metadata(uid: $uid, network: $network, sourceIP: $sourceIP, sourcePort: $sourcePort, destinationIP: $destinationIP, destinationPort: $destinationPort, host: $host, remoteDestination: $remoteDestination)';
return 'Metadata(uid: $uid, network: $network, sourceIP: $sourceIP, sourcePort: $sourcePort, destinationIP: $destinationIP, destinationPort: $destinationPort, host: $host, process: $process, remoteDestination: $remoteDestination)';
}
@override
@@ -240,14 +256,24 @@ class _$MetadataImpl implements _Metadata {
(identical(other.destinationPort, destinationPort) ||
other.destinationPort == destinationPort) &&
(identical(other.host, host) || other.host == host) &&
(identical(other.process, process) || other.process == process) &&
(identical(other.remoteDestination, remoteDestination) ||
other.remoteDestination == remoteDestination));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, uid, network, sourceIP,
sourcePort, destinationIP, destinationPort, host, remoteDestination);
int get hashCode => Object.hash(
runtimeType,
uid,
network,
sourceIP,
sourcePort,
destinationIP,
destinationPort,
host,
process,
remoteDestination);
@JsonKey(ignore: true)
@override
@@ -272,6 +298,7 @@ abstract class _Metadata implements Metadata {
required final String destinationIP,
required final String destinationPort,
required final String host,
required final String process,
required final String remoteDestination}) = _$MetadataImpl;
factory _Metadata.fromJson(Map<String, dynamic> json) =
@@ -292,6 +319,8 @@ abstract class _Metadata implements Metadata {
@override
String get host;
@override
String get process;
@override
String get remoteDestination;
@override
@JsonKey(ignore: true)

View File

@@ -15,6 +15,7 @@ _$MetadataImpl _$$MetadataImplFromJson(Map<String, dynamic> json) =>
destinationIP: json['destinationIP'] as String,
destinationPort: json['destinationPort'] as String,
host: json['host'] as String,
process: json['process'] as String,
remoteDestination: json['remoteDestination'] as String,
);
@@ -27,6 +28,7 @@ Map<String, dynamic> _$$MetadataImplToJson(_$MetadataImpl instance) =>
'destinationIP': instance.destinationIP,
'destinationPort': instance.destinationPort,
'host': instance.host,
'process': instance.process,
'remoteDestination': instance.remoteDestination,
};

View File

@@ -848,10 +848,8 @@ Process _$ProcessFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$Process {
int get uid => throw _privateConstructorUsedError;
String get network => throw _privateConstructorUsedError;
String get source => throw _privateConstructorUsedError;
String get target => throw _privateConstructorUsedError;
int get id => throw _privateConstructorUsedError;
Metadata get metadata => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -863,7 +861,9 @@ abstract class $ProcessCopyWith<$Res> {
factory $ProcessCopyWith(Process value, $Res Function(Process) then) =
_$ProcessCopyWithImpl<$Res, Process>;
@useResult
$Res call({int uid, String network, String source, String target});
$Res call({int id, Metadata metadata});
$MetadataCopyWith<$Res> get metadata;
}
/// @nodoc
@@ -879,30 +879,28 @@ class _$ProcessCopyWithImpl<$Res, $Val extends Process>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? uid = null,
Object? network = null,
Object? source = null,
Object? target = null,
Object? id = null,
Object? metadata = null,
}) {
return _then(_value.copyWith(
uid: null == uid
? _value.uid
: uid // ignore: cast_nullable_to_non_nullable
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
network: null == network
? _value.network
: network // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
target: null == target
? _value.target
: target // ignore: cast_nullable_to_non_nullable
as String,
metadata: null == metadata
? _value.metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Metadata,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$MetadataCopyWith<$Res> get metadata {
return $MetadataCopyWith<$Res>(_value.metadata, (value) {
return _then(_value.copyWith(metadata: value) as $Val);
});
}
}
/// @nodoc
@@ -912,7 +910,10 @@ abstract class _$$ProcessImplCopyWith<$Res> implements $ProcessCopyWith<$Res> {
__$$ProcessImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int uid, String network, String source, String target});
$Res call({int id, Metadata metadata});
@override
$MetadataCopyWith<$Res> get metadata;
}
/// @nodoc
@@ -926,28 +927,18 @@ class __$$ProcessImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? uid = null,
Object? network = null,
Object? source = null,
Object? target = null,
Object? id = null,
Object? metadata = null,
}) {
return _then(_$ProcessImpl(
uid: null == uid
? _value.uid
: uid // ignore: cast_nullable_to_non_nullable
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
network: null == network
? _value.network
: network // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
target: null == target
? _value.target
: target // ignore: cast_nullable_to_non_nullable
as String,
metadata: null == metadata
? _value.metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Metadata,
));
}
}
@@ -955,27 +946,19 @@ class __$$ProcessImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$ProcessImpl implements _Process {
const _$ProcessImpl(
{required this.uid,
required this.network,
required this.source,
required this.target});
const _$ProcessImpl({required this.id, required this.metadata});
factory _$ProcessImpl.fromJson(Map<String, dynamic> json) =>
_$$ProcessImplFromJson(json);
@override
final int uid;
final int id;
@override
final String network;
@override
final String source;
@override
final String target;
final Metadata metadata;
@override
String toString() {
return 'Process(uid: $uid, network: $network, source: $source, target: $target)';
return 'Process(id: $id, metadata: $metadata)';
}
@override
@@ -983,15 +966,14 @@ class _$ProcessImpl implements _Process {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProcessImpl &&
(identical(other.uid, uid) || other.uid == uid) &&
(identical(other.network, network) || other.network == network) &&
(identical(other.source, source) || other.source == source) &&
(identical(other.target, target) || other.target == target));
(identical(other.id, id) || other.id == id) &&
(identical(other.metadata, metadata) ||
other.metadata == metadata));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, uid, network, source, target);
int get hashCode => Object.hash(runtimeType, id, metadata);
@JsonKey(ignore: true)
@override
@@ -1009,27 +991,176 @@ class _$ProcessImpl implements _Process {
abstract class _Process implements Process {
const factory _Process(
{required final int uid,
required final String network,
required final String source,
required final String target}) = _$ProcessImpl;
{required final int id,
required final Metadata metadata}) = _$ProcessImpl;
factory _Process.fromJson(Map<String, dynamic> json) = _$ProcessImpl.fromJson;
@override
int get uid;
int get id;
@override
String get network;
@override
String get source;
@override
String get target;
Metadata get metadata;
@override
@JsonKey(ignore: true)
_$$ProcessImplCopyWith<_$ProcessImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ProcessMapItem _$ProcessMapItemFromJson(Map<String, dynamic> json) {
return _ProcessMapItem.fromJson(json);
}
/// @nodoc
mixin _$ProcessMapItem {
int get id => throw _privateConstructorUsedError;
String get value => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProcessMapItemCopyWith<ProcessMapItem> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProcessMapItemCopyWith<$Res> {
factory $ProcessMapItemCopyWith(
ProcessMapItem value, $Res Function(ProcessMapItem) then) =
_$ProcessMapItemCopyWithImpl<$Res, ProcessMapItem>;
@useResult
$Res call({int id, String value});
}
/// @nodoc
class _$ProcessMapItemCopyWithImpl<$Res, $Val extends ProcessMapItem>
implements $ProcessMapItemCopyWith<$Res> {
_$ProcessMapItemCopyWithImpl(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? id = null,
Object? value = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ProcessMapItemImplCopyWith<$Res>
implements $ProcessMapItemCopyWith<$Res> {
factory _$$ProcessMapItemImplCopyWith(_$ProcessMapItemImpl value,
$Res Function(_$ProcessMapItemImpl) then) =
__$$ProcessMapItemImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int id, String value});
}
/// @nodoc
class __$$ProcessMapItemImplCopyWithImpl<$Res>
extends _$ProcessMapItemCopyWithImpl<$Res, _$ProcessMapItemImpl>
implements _$$ProcessMapItemImplCopyWith<$Res> {
__$$ProcessMapItemImplCopyWithImpl(
_$ProcessMapItemImpl _value, $Res Function(_$ProcessMapItemImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? value = null,
}) {
return _then(_$ProcessMapItemImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ProcessMapItemImpl implements _ProcessMapItem {
const _$ProcessMapItemImpl({required this.id, required this.value});
factory _$ProcessMapItemImpl.fromJson(Map<String, dynamic> json) =>
_$$ProcessMapItemImplFromJson(json);
@override
final int id;
@override
final String value;
@override
String toString() {
return 'ProcessMapItem(id: $id, value: $value)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProcessMapItemImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.value, value) || other.value == value));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, id, value);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ProcessMapItemImplCopyWith<_$ProcessMapItemImpl> get copyWith =>
__$$ProcessMapItemImplCopyWithImpl<_$ProcessMapItemImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ProcessMapItemImplToJson(
this,
);
}
}
abstract class _ProcessMapItem implements ProcessMapItem {
const factory _ProcessMapItem(
{required final int id,
required final String value}) = _$ProcessMapItemImpl;
factory _ProcessMapItem.fromJson(Map<String, dynamic> json) =
_$ProcessMapItemImpl.fromJson;
@override
int get id;
@override
String get value;
@override
@JsonKey(ignore: true)
_$$ProcessMapItemImplCopyWith<_$ProcessMapItemImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ExternalProvider _$ExternalProviderFromJson(Map<String, dynamic> json) {
return _ExternalProvider.fromJson(json);
}

View File

@@ -56,6 +56,8 @@ const _$MessageTypeEnumMap = {
MessageType.delay: 'delay',
MessageType.process: 'process',
MessageType.now: 'now',
MessageType.request: 'request',
MessageType.run: 'run',
};
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(
@@ -81,18 +83,27 @@ Map<String, dynamic> _$$NowImplToJson(_$NowImpl instance) => <String, dynamic>{
_$ProcessImpl _$$ProcessImplFromJson(Map<String, dynamic> json) =>
_$ProcessImpl(
uid: (json['uid'] as num).toInt(),
network: json['network'] as String,
source: json['source'] as String,
target: json['target'] as String,
id: (json['id'] as num).toInt(),
metadata: Metadata.fromJson(json['metadata'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
<String, dynamic>{
'uid': instance.uid,
'network': instance.network,
'source': instance.source,
'target': instance.target,
'id': instance.id,
'metadata': instance.metadata,
};
_$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) =>
_$ProcessMapItemImpl(
id: (json['id'] as num).toInt(),
value: json['value'] as String,
);
Map<String, dynamic> _$$ProcessMapItemImplToJson(
_$ProcessMapItemImpl instance) =>
<String, dynamic>{
'id': instance.id,
'value': instance.value,
};
_$ExternalProviderImpl _$$ExternalProviderImplFromJson(

View File

@@ -24,10 +24,10 @@ Profile _$ProfileFromJson(Map<String, dynamic> json) => Profile(
id: json['id'] as String?,
label: json['label'] as String?,
url: json['url'] as String?,
currentGroupName: json['currentGroupName'] as String?,
userInfo: json['userInfo'] == null
? null
: UserInfo.fromJson(json['userInfo'] as Map<String, dynamic>),
proxyName: json['proxyName'] as String?,
lastUpdateDate: json['lastUpdateDate'] == null
? null
: DateTime.parse(json['lastUpdateDate'] as String),
@@ -43,7 +43,7 @@ Profile _$ProfileFromJson(Map<String, dynamic> json) => Profile(
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
'id': instance.id,
'label': instance.label,
'proxyName': instance.proxyName,
'currentGroupName': instance.currentGroupName,
'url': instance.url,
'lastUpdateDate': instance.lastUpdateDate?.toIso8601String(),
'autoUpdateDuration': instance.autoUpdateDuration.inMicroseconds,

View File

@@ -1583,6 +1583,7 @@ abstract class _ProxiesCardSelectorState implements ProxiesCardSelectorState {
/// @nodoc
mixin _$ProxiesSelectorState {
List<String> get groupNames => throw _privateConstructorUsedError;
String? get currentGroupName => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProxiesSelectorStateCopyWith<ProxiesSelectorState> get copyWith =>
@@ -1595,7 +1596,7 @@ abstract class $ProxiesSelectorStateCopyWith<$Res> {
$Res Function(ProxiesSelectorState) then) =
_$ProxiesSelectorStateCopyWithImpl<$Res, ProxiesSelectorState>;
@useResult
$Res call({List<String> groupNames});
$Res call({List<String> groupNames, String? currentGroupName});
}
/// @nodoc
@@ -1613,12 +1614,17 @@ class _$ProxiesSelectorStateCopyWithImpl<$Res,
@override
$Res call({
Object? groupNames = null,
Object? currentGroupName = freezed,
}) {
return _then(_value.copyWith(
groupNames: null == groupNames
? _value.groupNames
: groupNames // ignore: cast_nullable_to_non_nullable
as List<String>,
currentGroupName: freezed == currentGroupName
? _value.currentGroupName
: currentGroupName // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
@@ -1631,7 +1637,7 @@ abstract class _$$ProxiesSelectorStateImplCopyWith<$Res>
__$$ProxiesSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<String> groupNames});
$Res call({List<String> groupNames, String? currentGroupName});
}
/// @nodoc
@@ -1646,12 +1652,17 @@ class __$$ProxiesSelectorStateImplCopyWithImpl<$Res>
@override
$Res call({
Object? groupNames = null,
Object? currentGroupName = freezed,
}) {
return _then(_$ProxiesSelectorStateImpl(
groupNames: null == groupNames
? _value._groupNames
: groupNames // ignore: cast_nullable_to_non_nullable
as List<String>,
currentGroupName: freezed == currentGroupName
? _value.currentGroupName
: currentGroupName // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
@@ -1659,7 +1670,8 @@ class __$$ProxiesSelectorStateImplCopyWithImpl<$Res>
/// @nodoc
class _$ProxiesSelectorStateImpl implements _ProxiesSelectorState {
const _$ProxiesSelectorStateImpl({required final List<String> groupNames})
const _$ProxiesSelectorStateImpl(
{required final List<String> groupNames, required this.currentGroupName})
: _groupNames = groupNames;
final List<String> _groupNames;
@@ -1670,9 +1682,12 @@ class _$ProxiesSelectorStateImpl implements _ProxiesSelectorState {
return EqualUnmodifiableListView(_groupNames);
}
@override
final String? currentGroupName;
@override
String toString() {
return 'ProxiesSelectorState(groupNames: $groupNames)';
return 'ProxiesSelectorState(groupNames: $groupNames, currentGroupName: $currentGroupName)';
}
@override
@@ -1681,12 +1696,14 @@ class _$ProxiesSelectorStateImpl implements _ProxiesSelectorState {
(other.runtimeType == runtimeType &&
other is _$ProxiesSelectorStateImpl &&
const DeepCollectionEquality()
.equals(other._groupNames, _groupNames));
.equals(other._groupNames, _groupNames) &&
(identical(other.currentGroupName, currentGroupName) ||
other.currentGroupName == currentGroupName));
}
@override
int get hashCode => Object.hash(
runtimeType, const DeepCollectionEquality().hash(_groupNames));
int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(_groupNames), currentGroupName);
@JsonKey(ignore: true)
@override
@@ -1699,11 +1716,14 @@ class _$ProxiesSelectorStateImpl implements _ProxiesSelectorState {
abstract class _ProxiesSelectorState implements ProxiesSelectorState {
const factory _ProxiesSelectorState(
{required final List<String> groupNames}) = _$ProxiesSelectorStateImpl;
{required final List<String> groupNames,
required final String? currentGroupName}) = _$ProxiesSelectorStateImpl;
@override
List<String> get groupNames;
@override
String? get currentGroupName;
@override
@JsonKey(ignore: true)
_$$ProxiesSelectorStateImplCopyWith<_$ProxiesSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;

View File

@@ -62,7 +62,7 @@ class UserInfo {
class Profile {
String id;
String? label;
String? proxyName;
String? currentGroupName;
String? url;
DateTime? lastUpdateDate;
Duration autoUpdateDuration;
@@ -74,8 +74,8 @@ class Profile {
String? id,
this.label,
this.url,
this.currentGroupName,
this.userInfo,
this.proxyName,
this.lastUpdateDate,
SelectedMap? selectedMap,
Duration? autoUpdateDuration,
@@ -134,6 +134,7 @@ class Profile {
return _$ProfileFromJson(json);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -141,34 +142,31 @@ class Profile {
runtimeType == other.runtimeType &&
id == other.id &&
label == other.label &&
proxyName == other.proxyName &&
currentGroupName == other.currentGroupName &&
url == other.url &&
lastUpdateDate == other.lastUpdateDate &&
autoUpdateDuration == other.autoUpdateDuration &&
userInfo == other.userInfo &&
autoUpdate == other.autoUpdate;
autoUpdate == other.autoUpdate &&
selectedMap == other.selectedMap;
@override
int get hashCode =>
id.hashCode ^
label.hashCode ^
proxyName.hashCode ^
currentGroupName.hashCode ^
url.hashCode ^
lastUpdateDate.hashCode ^
autoUpdateDuration.hashCode ^
userInfo.hashCode ^
autoUpdate.hashCode;
@override
String toString() {
return 'Profile{id: $id, label: $label, proxyName: $proxyName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate}';
}
autoUpdate.hashCode ^
selectedMap.hashCode;
Profile copyWith({
String? label,
String? url,
UserInfo? userInfo,
String? groupName,
String? currentGroupName,
String? proxyName,
DateTime? lastUpdateDate,
Duration? autoUpdateDuration,
@@ -179,7 +177,7 @@ class Profile {
id: id,
label: label ?? this.label,
url: url ?? this.url,
proxyName: proxyName ?? this.proxyName,
currentGroupName: currentGroupName ?? this.currentGroupName,
userInfo: userInfo ?? this.userInfo,
selectedMap: selectedMap ?? this.selectedMap,
lastUpdateDate: lastUpdateDate ?? this.lastUpdateDate,

View File

@@ -94,6 +94,7 @@ class ProxiesCardSelectorState with _$ProxiesCardSelectorState {
class ProxiesSelectorState with _$ProxiesSelectorState {
const factory ProxiesSelectorState({
required List<String> groupNames,
required String? currentGroupName,
}) = _ProxiesSelectorState;
}

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

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -22,6 +23,9 @@ class App {
await onExit!();
}
break;
case "gc":
clashCore.requestGc();
break;
default:
throw MissingPluginException();
}
@@ -29,10 +33,6 @@ class App {
}
}
setOnExit(Function() onExit) {
this.onExit = onExit;
}
factory App() {
_instance ??= App._internal();
return _instance!;
@@ -68,9 +68,9 @@ class App {
});
}
Future<String?> getPackageName(Metadata metadata) async {
return await methodChannel?.invokeMethod<String>("getPackageName", {
"data": json.encode(metadata),
Future<String?> resolverProcess(Process process) async {
return await methodChannel?.invokeMethod<String>("resolverProcess", {
"data": json.encode(process),
});
}
}

View File

@@ -79,6 +79,10 @@ class Proxy extends ProxyPlatform {
}
}
// updateStartTime() async {
// startTime = clashCore.getRunTime();
// }
updateStartTime() async {
startTime = await getRunTime();
}

View File

@@ -17,7 +17,6 @@ class GlobalState {
Function? updateCurrentDelayDebounce;
PageController? pageController;
final navigatorKey = GlobalKey<NavigatorState>();
final Map<int, String?> packageNameMap = {};
late AppController appController;
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
List<Function> updateFunctionLists = [];
@@ -58,16 +57,28 @@ class GlobalState {
}
Future<void> startSystemProxy({
required AppState appState,
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,
);
startListenUpdate();
applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
Future<void> stopSystemProxy() async {
@@ -105,12 +116,6 @@ class GlobalState {
clashConfig: clashConfig,
);
}
if (!appState.isInit) return;
await applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
updateCoreVersionInfo(appState);
}
@@ -133,19 +138,6 @@ class GlobalState {
});
}
updateNavigationItems({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) {
final group = appState.currentGroups;
final hasProfile = config.profiles.isNotEmpty;
appState.navigationItems = navigation.getItems(
openLogs: config.openLogs,
hasProxies: group.isNotEmpty && hasProfile,
);
}
Future<void> updateGroups(AppState appState) async {
appState.groups = await clashCore.getProxiesGroups();
}
@@ -209,6 +201,7 @@ class GlobalState {
final traffic = clashCore.getTraffic();
if (appState != null) {
appState.addTraffic(traffic);
appState.totalTraffic = clashCore.getTotalTraffic();
}
if (Platform.isAndroid) {
final currentProfile = config.currentProfile;
@@ -225,30 +218,30 @@ class GlobalState {
required String message,
SnackBarAction? action,
}) {
final width = context.width;
EdgeInsets margin;
if (width < 600) {
margin = const EdgeInsets.only(
bottom: 96,
right: 16,
left: 16,
);
} else {
margin = EdgeInsets.only(
bottom: 16,
left: 16,
right: width - 316,
);
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
action: action,
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(milliseconds: 1500),
margin: margin,
),
);
// final width = context.width;
// EdgeInsets margin;
// if (width < 600) {
// margin = const EdgeInsets.only(
// bottom: 96,
// right: 16,
// left: 16,
// );
// } else {
// margin = EdgeInsets.only(
// bottom: 16,
// left: 16,
// right: width - 316,
// );
// }
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// action: action,
// content: Text(message),
// behavior: SnackBarBehavior.floating,
// duration: const Duration(milliseconds: 1500),
// margin: margin,
// ),
// );
}
Future<T?> safeRun<T>(

View File

@@ -1,6 +1,7 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/plugins/app.dart';
@@ -56,9 +57,26 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
}
@override
void onProcess(Metadata metadata) async {
var packageName = await app?.getPackageName(metadata);
globalState.packageNameMap[metadata.uid] = packageName;
super.onProcess(metadata);
void onProcess(Process process) async {
var packageName = await app?.resolverProcess(process);
clashCore.setProcessMap(
ProcessMapItem(
id: process.id,
value: packageName ?? "",
),
);
super.onProcess(process);
}
@override
void onRequest(Connection connection) async {
globalState.appController.appState.addRequest(connection);
super.onRequest(connection);
}
@override
void onRun(String runTime) async {
// proxy?.updateStartTime();
super.onRun(runTime);
}
}

View File

@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
class CommonScaffold extends StatefulWidget {
final Widget body;
final Widget? bottomNavigationBar;
final Widget? floatingActionButton;
final String title;
final Widget? leading;
final List<Widget>? actions;
@@ -19,6 +20,7 @@ class CommonScaffold extends StatefulWidget {
this.leading,
required this.title,
this.actions,
this.floatingActionButton,
this.automaticallyImplyLeading = true,
});
@@ -116,12 +118,14 @@ class CommonScaffoldState extends State<CommonScaffold> {
Widget build(BuildContext context) {
return _platformContainer(
child: Scaffold(
floatingActionButton: ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, floatingActionButton, __) {
return floatingActionButton ?? Container();
},
),
floatingActionButton: widget.floatingActionButton ??
ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, floatingActionButton, __) {
return floatingActionButton ?? Container();
},
),
resizeToAvoidBottomInset: true,
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.18
version: 0.8.25
environment:
sdk: '>=3.1.0 <4.0.0'
@@ -24,7 +24,7 @@ dependencies:
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
@@ -39,6 +39,8 @@ dependencies:
webdav_client: ^1.2.2
dio: ^5.4.3+1
country_flags: ^2.2.0
re_editor: ^0.3.0
re_highlight: ^0.0.3
dev_dependencies:
flutter_test:
sdk: flutter