Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23e3baf534 | ||
|
|
d522a64604 | ||
|
|
3902ea0064 | ||
|
|
5566f2b54f | ||
|
|
511b7c521a | ||
|
|
fb01d87371 |
17
README.md
17
README.md
@@ -6,13 +6,9 @@
|
||||
|
||||
## FlClash
|
||||
|
||||
<p style="text-align: left;">
|
||||
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
|
||||
<img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
|
||||
<a href="LICENSE">
|
||||
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
|
||||
</a>
|
||||
</p>
|
||||
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||
|
||||
[](https://t.me/FlClash)
|
||||
|
||||
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||
|
||||
@@ -42,10 +38,6 @@ on Mobile:
|
||||
|
||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
||||
|
||||
## Contact
|
||||
|
||||
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
|
||||
|
||||
## Build
|
||||
|
||||
1. Update submodules
|
||||
@@ -100,9 +92,6 @@ on Mobile:
|
||||
```bash
|
||||
dart .\setup.dart
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Star
|
||||
|
||||
|
||||
@@ -6,13 +6,10 @@
|
||||
|
||||
## FlClash
|
||||
|
||||
<p style="text-align: left;">
|
||||
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
|
||||
<img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
|
||||
<a href="LICENSE">
|
||||
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
|
||||
</a>
|
||||
</p>
|
||||
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||
|
||||
[](https://t.me/FlClash)
|
||||
|
||||
|
||||
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
|
||||
|
||||
@@ -42,11 +39,6 @@ on Mobile:
|
||||
|
||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
|
||||
|
||||
## Build
|
||||
|
||||
1. 更新 submodules
|
||||
|
||||
@@ -34,22 +34,22 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
|
||||
android {
|
||||
namespace "com.follow.clash"
|
||||
compileSdkVersion 34
|
||||
ndkVersion "25.1.8937393"
|
||||
ndkVersion "27.1.12297006"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
signingConfigs {
|
||||
if (isRelease){
|
||||
if (isRelease) {
|
||||
release {
|
||||
storeFile defStoreFile
|
||||
storePassword defStorePassword
|
||||
@@ -74,10 +74,9 @@ android {
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
if(isRelease){
|
||||
if (isRelease) {
|
||||
signingConfig signingConfigs.release
|
||||
}else{
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
|
||||
@@ -10,25 +10,22 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="tiramisu">
|
||||
android:hardwareAccelerated="true"
|
||||
android:label="FlClash">
|
||||
<activity
|
||||
android:name="com.follow.clash.MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
@@ -72,7 +69,17 @@
|
||||
|
||||
<activity
|
||||
android:name=".TempActivity"
|
||||
android:theme="@style/TransparentTheme" />
|
||||
android:exported="true"
|
||||
android:theme="@style/TransparentTheme">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="com.follow.clash.action.START" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="com.follow.clash.action.STOP" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
@@ -119,12 +126,19 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse" />
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="service" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
|
||||
@@ -33,7 +33,7 @@ object GlobalState {
|
||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||
}
|
||||
|
||||
fun getCurrentTitlePlugin(): TilePlugin? {
|
||||
fun getCurrentTilePlugin(): TilePlugin? {
|
||||
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
|
||||
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.follow.clash
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
@@ -9,7 +11,6 @@ import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(AppPlugin())
|
||||
|
||||
@@ -6,6 +6,15 @@ import android.os.Bundle
|
||||
class TempActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
when (intent.action) {
|
||||
"com.follow.clash.action.START" -> {
|
||||
GlobalState.getCurrentTilePlugin()?.handleStart()
|
||||
}
|
||||
|
||||
"com.follow.clash.action.STOP" -> {
|
||||
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||
}
|
||||
}
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,9 @@ fun Metadata.getProtocol(): Int? {
|
||||
}
|
||||
|
||||
|
||||
fun ConnectivityManager.resolvePrimaryDns(network: Network?): String? {
|
||||
val properties = getLinkProperties(network) ?: return null
|
||||
return properties.dnsServers.firstOrNull()?.asSocketAddressText(53)
|
||||
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
|
||||
val properties = getLinkProperties(network) ?: return listOf()
|
||||
return properties.dnsServers.map { it.asSocketAddressText(53) }
|
||||
}
|
||||
|
||||
fun InetAddress.asSocketAddressText(port: Int): String {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
|
||||
val TRANSPORT_PRIORITY = sequence {
|
||||
yield(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
yield(NetworkCapabilities.TRANSPORT_LOWPAN)
|
||||
}
|
||||
|
||||
yield(NetworkCapabilities.TRANSPORT_BLUETOOTH)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
yield(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
|
||||
}
|
||||
|
||||
yield(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
yield(NetworkCapabilities.TRANSPORT_USB)
|
||||
}
|
||||
|
||||
yield(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
}.toList()
|
||||
@@ -16,6 +16,7 @@ data class Props(
|
||||
val accessControl: AccessControl?,
|
||||
val allowBypass: Boolean?,
|
||||
val systemProxy: Boolean?,
|
||||
val ipv6: Boolean?,
|
||||
)
|
||||
|
||||
data class TunProps(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -16,7 +15,7 @@ import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.extensions.resolvePrimaryDns
|
||||
import com.follow.clash.extensions.resolveDns
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.models.TunProps
|
||||
import com.follow.clash.services.FlClashService
|
||||
@@ -163,8 +162,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
fun handleStartVpn() {
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
||||
start()
|
||||
}
|
||||
@@ -177,9 +175,11 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
val networks = mutableSetOf<Network>()
|
||||
|
||||
fun onUpdateNetwork() {
|
||||
val dns = networks.mapNotNull {
|
||||
connectivity?.resolvePrimaryDns(it)
|
||||
}.joinToString(separator = ",")
|
||||
val dns = networks.flatMap { network ->
|
||||
connectivity?.resolveDns(network) ?: emptyList()
|
||||
}
|
||||
.toSet()
|
||||
.joinToString(",")
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
||||
@@ -226,7 +226,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
onUpdateNetwork()
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
private fun startForeground(title: String, content: String) {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
|
||||
@@ -11,14 +11,13 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.models.Props
|
||||
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@@ -37,6 +38,7 @@ class FlClashTileService : TileService() {
|
||||
GlobalState.runState.observeForever(observer)
|
||||
}
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
private fun activityTransfer() {
|
||||
val intent = Intent(this, TempActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
@@ -67,15 +69,15 @@ class FlClashTileService : TileService() {
|
||||
activityTransfer()
|
||||
if (GlobalState.runState.value == RunState.STOP) {
|
||||
GlobalState.runState.value = RunState.PENDING
|
||||
val titlePlugin = GlobalState.getCurrentTitlePlugin()
|
||||
if (titlePlugin != null) {
|
||||
titlePlugin.handleStart()
|
||||
val tilePlugin = GlobalState.getCurrentTilePlugin()
|
||||
if (tilePlugin != null) {
|
||||
tilePlugin.handleStart()
|
||||
} else {
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
}
|
||||
} else if (GlobalState.runState.value == RunState.START) {
|
||||
GlobalState.runState.value = RunState.PENDING
|
||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
||||
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.TempActivity
|
||||
import com.follow.clash.models.AccessControlMode
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.models.TunProps
|
||||
@@ -29,7 +29,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
|
||||
companion object {
|
||||
@@ -73,11 +72,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
override fun start(port: Int, props: Props?): TunProps {
|
||||
return with(Builder()) {
|
||||
addAddress(TUN_GATEWAY, TUN_SUBNET_PREFIX)
|
||||
addAddress(TUN_GATEWAY6, TUN_SUBNET_PREFIX6)
|
||||
addRoute(NET_ANY, 0)
|
||||
addRoute(NET_ANY6, 0)
|
||||
addDnsServer(TUN_DNS)
|
||||
addDnsServer(TUN_DNS6)
|
||||
|
||||
|
||||
if (props?.ipv6 == true) {
|
||||
try {
|
||||
addAddress(TUN_GATEWAY6, TUN_SUBNET_PREFIX6)
|
||||
addRoute(NET_ANY6, 0)
|
||||
addDnsServer(TUN_DNS6)
|
||||
} catch (_: Exception) {
|
||||
|
||||
}
|
||||
}
|
||||
setMtu(TUN_MTU)
|
||||
props?.accessControl?.let { accessControl ->
|
||||
when (accessControl.mode) {
|
||||
@@ -115,16 +122,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
fd = establish()?.detachFd()
|
||||
?: throw NullPointerException("Establish VPN rejected by system"),
|
||||
gateway = "$TUN_GATEWAY/$TUN_SUBNET_PREFIX",
|
||||
gateway6 = "$TUN_GATEWAY6/$TUN_SUBNET_PREFIX6",
|
||||
gateway6 = if (props?.ipv6 == true) "$TUN_GATEWAY6/$TUN_SUBNET_PREFIX6" else "",
|
||||
portal = TUN_PORTAL,
|
||||
portal6 = TUN_PORTAL6,
|
||||
portal6 = if (props?.ipv6 == true) TUN_PORTAL6 else "",
|
||||
dns = TUN_DNS,
|
||||
dns6 = TUN_DNS6
|
||||
dns6 = if (props?.ipv6 == true) TUN_DNS6 else ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUnderlyingNetworks( networks: Array<Network>){
|
||||
fun updateUnderlyingNetworks(networks: Array<Network>) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
this.setUnderlyingNetworks(networks)
|
||||
}
|
||||
@@ -159,6 +166,27 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
val stopIntent = Intent(this, TempActivity::class.java)
|
||||
stopIntent.action = "com.follow.clash.action.STOP"
|
||||
stopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
|
||||
|
||||
val stopPendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
stopIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
stopIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
@@ -172,6 +200,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
addAction(0, "Stop", stopPendingIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +239,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
val isSuccess = super.onTransact(code, data, reply, flags)
|
||||
if (!isSuccess) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
||||
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||
}
|
||||
}
|
||||
return isSuccess
|
||||
|
||||
Binary file not shown.
Binary file not shown.
64257
assets/data/GeoSite.dat
64257
assets/data/GeoSite.dat
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -18,6 +18,7 @@ type AndroidProps struct {
|
||||
AccessControl *AccessControl `json:"accessControl"`
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
Ipv6 bool `json:"ipv6"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
|
||||
@@ -31,23 +31,26 @@ func Start(tunProps Props) (*sing_tun.Listener, error) {
|
||||
return nil, err
|
||||
}
|
||||
prefix4 = append(prefix4, tempPrefix4)
|
||||
|
||||
var prefix6 []netip.Prefix
|
||||
tempPrefix6, err := netip.ParsePrefix(tunProps.Gateway6)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
if tunProps.Gateway6 != "" {
|
||||
tempPrefix6, err := netip.ParsePrefix(tunProps.Gateway6)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
}
|
||||
prefix6 = append(prefix6, tempPrefix6)
|
||||
}
|
||||
prefix6 = append(prefix6, tempPrefix6)
|
||||
|
||||
var dnsHijack []string
|
||||
dnsHijack = append(dnsHijack, net.JoinHostPort(tunProps.Dns, "53"))
|
||||
dnsHijack = append(dnsHijack, net.JoinHostPort(tunProps.Dns6, "53"))
|
||||
if tunProps.Dns6 != "" {
|
||||
dnsHijack = append(dnsHijack, net.JoinHostPort(tunProps.Dns6, "53"))
|
||||
}
|
||||
|
||||
options := LC.Tun{
|
||||
Enable: true,
|
||||
Device: sing_tun.InterfaceName,
|
||||
Stack: constant.TunSystem,
|
||||
Stack: constant.TunMixed,
|
||||
DNSHijack: dnsHijack,
|
||||
AutoRoute: false,
|
||||
AutoDetectInterface: false,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -29,6 +27,9 @@ runAppWithPreferences(
|
||||
ChangeNotifierProvider<Config>(
|
||||
create: (_) => config,
|
||||
),
|
||||
ChangeNotifierProvider<AppFlowingState>(
|
||||
create: (_) => AppFlowingState(),
|
||||
),
|
||||
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
||||
create: (_) => appState,
|
||||
update: (_, config, clashConfig, appState) {
|
||||
@@ -85,6 +86,7 @@ class ApplicationState extends State<Application> {
|
||||
super.initState();
|
||||
_initTimer();
|
||||
globalState.appController = AppController(context);
|
||||
globalState.measure = Measure.of(context);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final currentContext = globalState.navigatorKey.currentContext;
|
||||
if (currentContext != null) {
|
||||
@@ -179,8 +181,15 @@ class ApplicationState extends State<Application> {
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return MediaManager(
|
||||
child: _buildPage(child!),
|
||||
return LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
final appController = globalState.appController;
|
||||
final maxWidth = container.maxWidth;
|
||||
if (appController.appState.viewWidth != maxWidth) {
|
||||
globalState.appController.updateViewWidth(maxWidth);
|
||||
}
|
||||
return _buildPage(child!);
|
||||
},
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class Android {
|
||||
init() async {
|
||||
app?.onExit = () {};
|
||||
app?.onExit = () async {
|
||||
await globalState.appController.savePreferences();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,5 @@ export 'iterable.dart';
|
||||
export 'scroll.dart';
|
||||
export 'icons.dart';
|
||||
export 'http.dart';
|
||||
export 'keyboard.dart';
|
||||
export 'keyboard.dart';
|
||||
export 'network.dart';
|
||||
@@ -26,9 +26,9 @@ const GeoXMap defaultGeoXMap = {
|
||||
"mmdb":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||
"asn":
|
||||
"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
|
||||
"geoip":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat",
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
|
||||
"geosite":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ extension BuildContextExtension on BuildContext {
|
||||
return MediaQuery.of(this).size;
|
||||
}
|
||||
|
||||
double get width {
|
||||
double get viewWidth {
|
||||
return appSize.width;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ class FlClashHttpOverrides extends HttpOverrides {
|
||||
client.badCertificateCallback = (_, __, ___) => true;
|
||||
client.findProxy = (url) {
|
||||
debugPrint("find $url");
|
||||
final port = globalState.appController.clashConfig.mixedPort;
|
||||
final isStart = globalState.appController.appState.isStart;
|
||||
final appController = globalState.appController;
|
||||
final port = appController.clashConfig.mixedPort;
|
||||
final isStart = appController.appFlowingState.isStart;
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
};
|
||||
|
||||
@@ -62,6 +62,6 @@ extension DoubleListExt on List<double> {
|
||||
}
|
||||
}
|
||||
|
||||
return -1; // 这行理论上不会执行到,但为了完整性保留
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,10 @@ extension ListExtension<T> on List<T> {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
List<T> safeSublist(int start) {
|
||||
if(start <= 0) return this;
|
||||
if(start > length) return [];
|
||||
return sublist(start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ class Measure {
|
||||
final TextScaler _textScale;
|
||||
late BuildContext context;
|
||||
|
||||
Measure.of(this.context) : _textScale = MediaQuery.of(context).textScaler;
|
||||
Measure.of(this.context)
|
||||
: _textScale = TextScaler.linear(
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor);
|
||||
|
||||
Size computeTextSize(Text text) {
|
||||
final textPainter = TextPainter(
|
||||
|
||||
25
lib/common/network.dart
Normal file
25
lib/common/network.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
|
||||
extension NetworkInterfaceExt on NetworkInterface {
|
||||
bool get isWifi {
|
||||
final nameLowCase = name.toLowerCase();
|
||||
if (nameLowCase.contains('wlan') ||
|
||||
nameLowCase.contains('wi-fi') ||
|
||||
nameLowCase == 'en0' ||
|
||||
nameLowCase == 'eth0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get includesIPv4 {
|
||||
return addresses.any((addr) => addr.isIPv4);
|
||||
}
|
||||
}
|
||||
|
||||
extension InternetAddressExt on InternetAddress {
|
||||
bool get isIPv4 {
|
||||
return type == InternetAddressType.IPv4;
|
||||
}
|
||||
}
|
||||
@@ -171,14 +171,18 @@ class Other {
|
||||
if (disposition == null) return null;
|
||||
final parseValue = HeaderValue.parse(disposition);
|
||||
final parameters = parseValue.parameters;
|
||||
final key = parameters.keys
|
||||
.firstWhere((key) => key.startsWith("filename"), orElse: () => '');
|
||||
if (key.isEmpty) return null;
|
||||
if (key == "filename*") {
|
||||
return Uri.decodeComponent((parameters[key] ?? "").split("'").last);
|
||||
} else {
|
||||
return parameters[key];
|
||||
final fileNamePointKey = parameters.keys
|
||||
.firstWhere((key) => key == "filename*", orElse: () => "");
|
||||
if (fileNamePointKey.isNotEmpty) {
|
||||
final res = parameters[fileNamePointKey]?.split("''") ?? [];
|
||||
if (res.length >= 2) {
|
||||
return Uri.decodeComponent(res[1]);
|
||||
}
|
||||
}
|
||||
final fileNameKey = parameters.keys
|
||||
.firstWhere((key) => key == "filename", orElse: () => "");
|
||||
if (fileNameKey.isEmpty) return null;
|
||||
return parameters[fileNameKey];
|
||||
}
|
||||
|
||||
double getViewWidth() {
|
||||
@@ -220,6 +224,15 @@ class Other {
|
||||
String getBackupFileName() {
|
||||
return "${appName}_backup_${DateTime.now().show}.zip";
|
||||
}
|
||||
|
||||
String get logFile {
|
||||
return "${appName}_${DateTime.now().show}.log";
|
||||
}
|
||||
|
||||
Size getScreenSize() {
|
||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
return view.physicalSize / view.devicePixelRatio;
|
||||
}
|
||||
}
|
||||
|
||||
final other = Other();
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'window.dart';
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:fl_clash/common/archive.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart';
|
||||
@@ -19,6 +21,7 @@ import 'common/common.dart';
|
||||
class AppController {
|
||||
final BuildContext context;
|
||||
late AppState appState;
|
||||
late AppFlowingState appFlowingState;
|
||||
late Config config;
|
||||
late ClashConfig clashConfig;
|
||||
late Function updateClashConfigDebounce;
|
||||
@@ -30,6 +33,7 @@ class AppController {
|
||||
appState = context.read<AppState>();
|
||||
config = context.read<Config>();
|
||||
clashConfig = context.read<ClashConfig>();
|
||||
appFlowingState = context.read<AppFlowingState>();
|
||||
updateClashConfigDebounce = debounce<Function()>(() async {
|
||||
await updateClashConfig();
|
||||
});
|
||||
@@ -56,13 +60,15 @@ class AppController {
|
||||
updateRunTime,
|
||||
updateTraffic,
|
||||
];
|
||||
applyProfileDebounce();
|
||||
if (!Platform.isAndroid) {
|
||||
applyProfileDebounce();
|
||||
}
|
||||
} else {
|
||||
await globalState.handleStop();
|
||||
clashCore.resetTraffic();
|
||||
appState.traffics = [];
|
||||
appState.totalTraffic = Traffic();
|
||||
appState.runTime = null;
|
||||
appFlowingState.traffics = [];
|
||||
appFlowingState.totalTraffic = Traffic();
|
||||
appFlowingState.runTime = null;
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
}
|
||||
@@ -76,15 +82,15 @@ class AppController {
|
||||
if (startTime != null) {
|
||||
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
||||
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||
appState.runTime = nowTimeStamp - startTimeStamp;
|
||||
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
|
||||
} else {
|
||||
appState.runTime = null;
|
||||
appFlowingState.runTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateTraffic() {
|
||||
globalState.updateTraffic(
|
||||
appState: appState,
|
||||
appFlowingState: appFlowingState,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,11 +122,15 @@ class AppController {
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||
await globalState.updateClashConfig(
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
isPatch: isPatch,
|
||||
);
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.updateClashConfig(
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
isPatch: isPatch,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future applyProfile({bool isPrue = false}) async {
|
||||
@@ -163,7 +173,7 @@ class AppController {
|
||||
try {
|
||||
updateProfile(profile);
|
||||
} catch (e) {
|
||||
appState.addLog(
|
||||
appFlowingState.addLog(
|
||||
Log(
|
||||
logLevel: LogLevel.info,
|
||||
payload: e.toString(),
|
||||
@@ -241,7 +251,7 @@ class AppController {
|
||||
clashCore.startLog();
|
||||
} else {
|
||||
clashCore.stopLog();
|
||||
appState.logs = [];
|
||||
appFlowingState.logs = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +307,7 @@ class AppController {
|
||||
init() async {
|
||||
final isDisclaimerAccepted = await handlerDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
system.exit();
|
||||
handleExit();
|
||||
}
|
||||
updateLogStatus();
|
||||
if (!config.silentLaunch) {
|
||||
@@ -518,21 +528,6 @@ class AppController {
|
||||
'';
|
||||
}
|
||||
|
||||
Future<List<int>> backupData() async {
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
final profilesPath = await appPath.getProfilesPath();
|
||||
final configJson = config.toJson();
|
||||
final clashConfigJson = clashConfig.toJson();
|
||||
return Isolate.run<List<int>>(() async {
|
||||
final archive = Archive();
|
||||
archive.add("config.json", configJson);
|
||||
archive.add("clashConfig.json", clashConfigJson);
|
||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||
final zipEncoder = ZipEncoder();
|
||||
return zipEncoder.encode(archive) ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
@@ -546,7 +541,7 @@ class AppController {
|
||||
}
|
||||
|
||||
updateStart() {
|
||||
updateStatus(!appState.isStart);
|
||||
updateStatus(!appFlowingState.isStart);
|
||||
}
|
||||
|
||||
updateAutoLaunch() {
|
||||
@@ -571,6 +566,36 @@ class AppController {
|
||||
clashConfig.mode = Mode.values[nextIndex];
|
||||
}
|
||||
|
||||
Future<bool> exportLogs() async {
|
||||
final logsRaw = appFlowingState.logs.map(
|
||||
(item) => item.toString(),
|
||||
);
|
||||
final data = await Isolate.run<List<int>>(() async {
|
||||
final logsRawString = logsRaw.join("\n");
|
||||
return utf8.encode(logsRawString);
|
||||
});
|
||||
return await picker.saveFile(
|
||||
other.logFile,
|
||||
Uint8List.fromList(data),
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
|
||||
Future<List<int>> backupData() async {
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
final profilesPath = await appPath.getProfilesPath();
|
||||
final configJson = config.toJson();
|
||||
final clashConfigJson = clashConfig.toJson();
|
||||
return Isolate.run<List<int>>(() async {
|
||||
final archive = Archive();
|
||||
archive.add("config.json", configJson);
|
||||
archive.add("clashConfig.json", clashConfigJson);
|
||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||
final zipEncoder = ZipEncoder();
|
||||
return zipEncoder.encode(archive) ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
recoveryData(
|
||||
List<int> data,
|
||||
RecoveryOption recoveryOption,
|
||||
|
||||
@@ -15,6 +15,10 @@ extension GroupTypeExtension on GroupType {
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool get isURLTestOrFallback {
|
||||
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
||||
}
|
||||
|
||||
static GroupType? getGroupType(String value) {
|
||||
final index = GroupTypeExtension.valueList.indexOf(value);
|
||||
if (index == -1) return null;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
@@ -67,11 +67,9 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
title: const Text("DNS"),
|
||||
subtitle: Text(appLocalizations.dnsDesc),
|
||||
leading: const Icon(Icons.dns),
|
||||
delegate: OpenDelegate(
|
||||
delegate: const OpenDelegate(
|
||||
title: "DNS",
|
||||
widget: generateListView(
|
||||
dnsItems,
|
||||
),
|
||||
widget: DnsListView(),
|
||||
isScaffold: true,
|
||||
isBlur: false,
|
||||
extendPageWidth: 360,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/app_localizations.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
@@ -11,27 +10,8 @@ import 'package:provider/provider.dart';
|
||||
class OverrideItem extends StatelessWidget {
|
||||
const OverrideItem({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.appController.clashConfig.dns = const Dns();
|
||||
},
|
||||
tooltip: appLocalizations.resetDns,
|
||||
icon: const Icon(
|
||||
Icons.replay,
|
||||
),
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.overrideDns,
|
||||
builder: (_, override, __) {
|
||||
@@ -51,35 +31,6 @@ class OverrideItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DnsDisabledContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const DnsDisabledContainer(
|
||||
this.child, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.overrideDns,
|
||||
builder: (_, enable, child) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !enable,
|
||||
child: DisabledMask(
|
||||
status: !enable,
|
||||
child: Container(
|
||||
color: context.colorScheme.surface,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusItem extends StatelessWidget {
|
||||
const StatusItem({super.key});
|
||||
|
||||
@@ -462,7 +413,6 @@ class NameserverPolicyItem extends StatelessWidget {
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
isMap: true,
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
@@ -788,27 +738,25 @@ class DnsOptions extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DnsDisabledContainer(
|
||||
Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.options,
|
||||
items: [
|
||||
const StatusItem(),
|
||||
const UseHostsItem(),
|
||||
const UseSystemHostsItem(),
|
||||
const IPv6Item(),
|
||||
const RespectRulesItem(),
|
||||
const PreferH3Item(),
|
||||
const DnsModeItem(),
|
||||
const FakeIpRangeItem(),
|
||||
const FakeIpFilterItem(),
|
||||
const DefaultNameserverItem(),
|
||||
const NameserverPolicyItem(),
|
||||
const NameserverItem(),
|
||||
const FallbackItem(),
|
||||
const ProxyServerNameserverItem(),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.options,
|
||||
items: [
|
||||
const StatusItem(),
|
||||
const UseHostsItem(),
|
||||
const UseSystemHostsItem(),
|
||||
const IPv6Item(),
|
||||
const RespectRulesItem(),
|
||||
const PreferH3Item(),
|
||||
const DnsModeItem(),
|
||||
const FakeIpRangeItem(),
|
||||
const FakeIpFilterItem(),
|
||||
const DefaultNameserverItem(),
|
||||
const NameserverPolicyItem(),
|
||||
const NameserverItem(),
|
||||
const FallbackItem(),
|
||||
const ProxyServerNameserverItem(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -819,18 +767,16 @@ class FallbackFilterOptions extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DnsDisabledContainer(
|
||||
Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.fallbackFilter,
|
||||
items: [
|
||||
const GeoipItem(),
|
||||
const GeoipCodeItem(),
|
||||
const GeositeItem(),
|
||||
const IpcidrItem(),
|
||||
const DomainItem(),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.fallbackFilter,
|
||||
items: [
|
||||
const GeoipItem(),
|
||||
const GeoipCodeItem(),
|
||||
const GeositeItem(),
|
||||
const IpcidrItem(),
|
||||
const DomainItem(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -841,3 +787,41 @@ const dnsItems = <Widget>[
|
||||
DnsOptions(),
|
||||
FallbackFilterOptions(),
|
||||
];
|
||||
|
||||
class DnsListView extends StatelessWidget {
|
||||
const DnsListView({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.resetDns,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.dnsResetTip,
|
||||
),
|
||||
onTab: () {
|
||||
globalState.appController.clashConfig.dns = const Dns();
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
},
|
||||
tooltip: appLocalizations.resetDns,
|
||||
icon: const Icon(
|
||||
Icons.replay,
|
||||
),
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
return generateListView(
|
||||
dnsItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,6 @@ class HostsItem extends StatelessWidget {
|
||||
clashConfig.hosts = Map.from(clashConfig.hosts)
|
||||
..addEntries([value]);
|
||||
},
|
||||
isMap: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -115,6 +115,34 @@ class SystemProxySwitch extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class Ipv6Switch extends StatelessWidget {
|
||||
const Ipv6Switch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.water_outlined),
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
ipv6: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VpnOptions extends StatelessWidget {
|
||||
const VpnOptions({super.key});
|
||||
|
||||
@@ -127,6 +155,7 @@ class VpnOptions extends StatelessWidget {
|
||||
items: [
|
||||
const SystemProxySwitch(),
|
||||
const AllowBypassSwitch(),
|
||||
const Ipv6Switch(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
|
||||
@@ -13,16 +13,55 @@ class IntranetIP extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _IntranetIPState extends State<IntranetIP> {
|
||||
final ipNotifier = ValueNotifier<String>("");
|
||||
final ipNotifier = ValueNotifier<String?>("");
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list();
|
||||
for (final interface in interfaces) {
|
||||
for (final address in interface.addresses) {
|
||||
if (!address.isLoopback) {
|
||||
return address.address;
|
||||
Future<String> getNetworkType() async {
|
||||
try {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
type: InternetAddressType.any,
|
||||
);
|
||||
|
||||
for (var interface in interfaces) {
|
||||
if (interface.name.toLowerCase().contains('wlan') ||
|
||||
interface.name.toLowerCase().contains('wi-fi')) {
|
||||
return 'WiFi';
|
||||
}
|
||||
if (interface.name.toLowerCase().contains('rmnet') ||
|
||||
interface.name.toLowerCase().contains('ccmni') ||
|
||||
interface.name.toLowerCase().contains('cellular')) {
|
||||
return 'Mobile Data';
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
} catch (e) {
|
||||
return 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
)
|
||||
..sort((a, b) {
|
||||
if (a.isWifi && !b.isWifi) return -1;
|
||||
if (!a.isWifi && b.isWifi) return 1;
|
||||
if (a.includesIPv4 && !b.includesIPv4) return -1;
|
||||
if (!a.includesIPv4 && b.includesIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
for (final interface in interfaces) {
|
||||
final addresses = interface.addresses;
|
||||
if (addresses.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
addresses.sort((a, b) {
|
||||
if (a.isIPv4 && !b.isIPv4) return -1;
|
||||
if (!a.isIPv4 && b.isIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
return addresses.first.address;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -48,17 +87,15 @@ class _IntranetIPState extends State<IntranetIP> {
|
||||
label: appLocalizations.intranetIP,
|
||||
iconData: Icons.devices,
|
||||
),
|
||||
onPressed: (){
|
||||
|
||||
},
|
||||
onPressed: () {},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
height: globalState.measure.titleLargeHeight + 24 - 2,
|
||||
height: globalState.measure.titleMediumHeight + 24 - 2,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: ipNotifier,
|
||||
builder: (_, value, __) {
|
||||
return FadeBox(
|
||||
child: value.isNotEmpty
|
||||
child: value != null
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -67,8 +104,9 @@ class _IntranetIPState extends State<IntranetIP> {
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
value,
|
||||
style: context.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||
value.isNotEmpty ? value : appLocalizations.noNetwork,
|
||||
style: context
|
||||
.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
@@ -22,14 +24,17 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
);
|
||||
bool? _preIsStart;
|
||||
Function? _checkIpDebounce;
|
||||
Timer? _setTimeoutTimer;
|
||||
CancelToken? cancelToken;
|
||||
|
||||
_checkIp() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
final isInit = appState.isInit;
|
||||
if (!isInit) return;
|
||||
final isStart = appState.isStart;
|
||||
final isStart = appFlowingState.isStart;
|
||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||
_clearSetTimeoutTimer();
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
@@ -42,11 +47,32 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
cancelToken = CancelToken();
|
||||
try {
|
||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
||||
if (ipInfo != null) {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_setTimeoutTimer = Timer(const Duration(milliseconds: 2000), () {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: null,
|
||||
);
|
||||
});
|
||||
} catch (_) {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
_clearSetTimeoutTimer() {
|
||||
if(_setTimeoutTimer != null){
|
||||
_setTimeoutTimer?.cancel();
|
||||
_setTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_checkIpContainer(Widget child) {
|
||||
|
||||
@@ -116,8 +116,8 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
label: appLocalizations.networkSpeed,
|
||||
iconData: Icons.speed_sharp,
|
||||
),
|
||||
child: Selector<AppState, List<Traffic>>(
|
||||
selector: (_, appState) => appState.traffics,
|
||||
child: Selector<AppFlowingState, List<Traffic>>(
|
||||
selector: (_, appFlowingState) => appFlowingState.traffics,
|
||||
builder: (_, traffics, __) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
||||
@@ -34,7 +34,7 @@ class _StartButtonState extends State<StartButton>
|
||||
|
||||
handleSwitchStart() {
|
||||
final appController = globalState.appController;
|
||||
if (isStart == appController.appState.isStart) {
|
||||
if (isStart == appController.appFlowingState.isStart) {
|
||||
isStart = !isStart;
|
||||
updateController();
|
||||
appController.updateStatus(isStart);
|
||||
@@ -50,8 +50,8 @@ class _StartButtonState extends State<StartButton>
|
||||
}
|
||||
|
||||
Widget _updateControllerContainer(Widget child) {
|
||||
return Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.isStart,
|
||||
return Selector<AppFlowingState, bool>(
|
||||
selector: (_, appFlowingState) => appFlowingState.isStart,
|
||||
builder: (_, isStart, child) {
|
||||
if (isStart != this.isStart) {
|
||||
this.isStart = isStart;
|
||||
@@ -127,8 +127,8 @@ class _StartButtonState extends State<StartButton>
|
||||
);
|
||||
},
|
||||
child: _updateControllerContainer(
|
||||
Selector<AppState, int?>(
|
||||
selector: (_, appState) => appState.runTime,
|
||||
Selector<AppFlowingState, int?>(
|
||||
selector: (_, appFlowingState) => appFlowingState.runTime,
|
||||
builder: (_, int? value, __) {
|
||||
final text = other.getTimeText(value);
|
||||
return Text(
|
||||
|
||||
@@ -56,8 +56,8 @@ class TrafficUsage extends StatelessWidget {
|
||||
label: appLocalizations.trafficUsage,
|
||||
iconData: Icons.data_saver_off,
|
||||
),
|
||||
child: Selector<AppState, Traffic>(
|
||||
selector: (_, appState) => appState.totalTraffic,
|
||||
child: Selector<AppFlowingState, Traffic>(
|
||||
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
||||
builder: (_, totalTraffic, __) {
|
||||
final upTotalTrafficValue = totalTraffic.up;
|
||||
final downTotalTrafficValue = totalTraffic.down;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -29,19 +30,25 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
logsNotifier.value = logsNotifier.value.copyWith(logs: appState.logs);
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
logsNotifier.value =
|
||||
logsNotifier.value.copyWith(logs: appFlowingState.logs);
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
final logs = appState.logs;
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
final logs = appFlowingState.logs.safeSublist(
|
||||
appFlowingState.logs.length - maxLength,
|
||||
);
|
||||
if (!const ListEquality<Log>().equals(
|
||||
logsNotifier.value.logs,
|
||||
logs,
|
||||
)) {
|
||||
logsNotifier.value = logsNotifier.value.copyWith(logs: logs);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
logs: logs,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -56,6 +63,21 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
timer = null;
|
||||
}
|
||||
|
||||
_handleExport() async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
return await globalState.appController.exportLogs();
|
||||
},
|
||||
title: appLocalizations.exportLogs,
|
||||
);
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.exportSuccess),
|
||||
);
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
@@ -72,6 +94,17 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_handleExport();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.file_download_outlined,
|
||||
),
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -236,7 +269,8 @@ class LogsSearchDelegate extends SearchDelegate {
|
||||
_addKeyword(String keyword) {
|
||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||
if (isContains) return;
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)..add(keyword);
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||
..add(keyword);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
@@ -245,7 +279,8 @@ class LogsSearchDelegate extends SearchDelegate {
|
||||
_deleteKeyword(String keyword) {
|
||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||
if (!isContains) return;
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)..remove(keyword);
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||
..remove(keyword);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
@@ -339,7 +374,9 @@ class _LogItemState extends State<LogItem> {
|
||||
style: context.textTheme.bodySmall
|
||||
?.copyWith(color: context.colorScheme.primary),
|
||||
),
|
||||
const SizedBox(height: 8,),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CommonChip(
|
||||
|
||||
@@ -102,12 +102,12 @@ class ProxyCard extends StatelessWidget {
|
||||
|
||||
_changeProxy(BuildContext context) async {
|
||||
final appController = globalState.appController;
|
||||
final isUrlTest = groupType == GroupType.URLTest;
|
||||
final isURLTestOrFallback = groupType.isURLTestOrFallback;
|
||||
final isSelector = groupType == GroupType.Selector;
|
||||
if (isUrlTest || isSelector) {
|
||||
if (isURLTestOrFallback || isSelector) {
|
||||
final currentProxyName =
|
||||
appController.config.currentSelectedMap[groupName];
|
||||
final nextProxyName = switch (isUrlTest) {
|
||||
final nextProxyName = switch (isURLTestOrFallback) {
|
||||
true => currentProxyName == proxy.name ? "" : proxy.name,
|
||||
false => proxy.name,
|
||||
};
|
||||
@@ -133,7 +133,7 @@ class ProxyCard extends StatelessWidget {
|
||||
final measure = globalState.measure;
|
||||
final delayText = _buildDelayText();
|
||||
final proxyNameText = _buildProxyNameText(context);
|
||||
return currentGroupProxyNameBuilder(
|
||||
return currentSelectedProxyNameBuilder(
|
||||
groupName: groupName,
|
||||
builder: (currentGroupName) {
|
||||
return Stack(
|
||||
@@ -207,30 +207,16 @@ class ProxyCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (groupType == GroupType.URLTest)
|
||||
if (groupType.isURLTestOrFallback)
|
||||
Selector<Config, String>(
|
||||
selector: (_, config) {
|
||||
final selectedProxyName =
|
||||
config.currentSelectedMap[groupName];
|
||||
return selectedProxyName ?? '';
|
||||
},
|
||||
builder: (_, value, __) {
|
||||
builder: (_, value, child) {
|
||||
if (value != proxy.name) return Container();
|
||||
return Positioned.fill(
|
||||
child: Container(
|
||||
alignment: Alignment.topRight,
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: const SelectIcon(),
|
||||
),
|
||||
),
|
||||
);
|
||||
return child!;
|
||||
},
|
||||
child: Positioned.fill(
|
||||
child: Container(
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Widget currentGroupProxyNameBuilder({
|
||||
Widget currentSelectedProxyNameBuilder({
|
||||
required String groupName,
|
||||
required Widget Function(String currentGroupName) builder,
|
||||
}) {
|
||||
@@ -16,8 +16,8 @@ Widget currentGroupProxyNameBuilder({
|
||||
final selectedProxyName = config.currentSelectedMap[groupName];
|
||||
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
|
||||
},
|
||||
builder: (_, currentGroupName, ___) {
|
||||
return builder(currentGroupName);
|
||||
builder: (_, currentSelectedProxyName, ___) {
|
||||
return builder(currentSelectedProxyName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -487,7 +487,7 @@ class _ListHeaderState extends State<ListHeader>
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: currentGroupProxyNameBuilder(
|
||||
child: currentSelectedProxyNameBuilder(
|
||||
groupName: groupName,
|
||||
builder: (currentGroupName) {
|
||||
return Row(
|
||||
|
||||
@@ -321,7 +321,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 80,
|
||||
bottom: 96,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -37,7 +38,10 @@ class _RequestsFragmentState extends State<RequestsFragment> {
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
final requests = appState.requests;
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
final requests = appState.requests.safeSublist(
|
||||
appState.requests.length - maxLength,
|
||||
);
|
||||
if (!const ListEquality<Connection>().equals(
|
||||
requestsNotifier.value.connections,
|
||||
requests,
|
||||
|
||||
@@ -70,7 +70,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
final isDisclaimerAccepted =
|
||||
await globalState.appController.showDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
system.exit();
|
||||
globalState.appController.handleExit();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"tabAnimation": "Tab animation",
|
||||
"tabAnimationDesc": "When enabled, the home tab will add a toggle animation",
|
||||
"desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
|
||||
"startVpn": "Staring VPN...",
|
||||
"startVpn": "Starting VPN...",
|
||||
"stopVpn": "Stopping VPN...",
|
||||
"discovery": "Discovery a new version",
|
||||
"compatible": "Compatibility mode",
|
||||
@@ -303,5 +303,10 @@
|
||||
"inputCorrectHotkey": "Please enter the correct hotkey",
|
||||
"hotkeyConflict": "Hotkey conflict",
|
||||
"remove": "Remove",
|
||||
"noHotKey": "No HotKey"
|
||||
"noHotKey": "No HotKey",
|
||||
"dnsResetTip": "Make sure to reset the DNS",
|
||||
"noNetwork": "No network",
|
||||
"ipv6InboundDesc": "Allow IPv6 inbound",
|
||||
"exportLogs": "Export logs",
|
||||
"exportSuccess": "Export Success"
|
||||
}
|
||||
@@ -303,5 +303,10 @@
|
||||
"inputCorrectHotkey": "请输入正确的快捷键",
|
||||
"hotkeyConflict": "快捷键冲突",
|
||||
"remove": "移除",
|
||||
"noHotKey": "暂无快捷键"
|
||||
"noHotKey": "暂无快捷键",
|
||||
"dnsResetTip": "确定重置DNS",
|
||||
"noNetwork": "无网络",
|
||||
"ipv6InboundDesc": "允许IPv6入站",
|
||||
"exportLogs": "导出日志",
|
||||
"exportSuccess": "导出成功"
|
||||
}
|
||||
@@ -146,6 +146,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"dnsDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Update DNS related settings"),
|
||||
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS mode"),
|
||||
"dnsResetTip":
|
||||
MessageLookupByLibrary.simpleMessage("Make sure to reset the DNS"),
|
||||
"doYouWantToPass":
|
||||
MessageLookupByLibrary.simpleMessage("Do you want to pass"),
|
||||
"domain": MessageLookupByLibrary.simpleMessage("Domain"),
|
||||
@@ -161,6 +163,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||
"expirationTime":
|
||||
MessageLookupByLibrary.simpleMessage("Expiration time"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("Export logs"),
|
||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("Export Success"),
|
||||
"externalController":
|
||||
MessageLookupByLibrary.simpleMessage("ExternalController"),
|
||||
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -217,6 +221,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"),
|
||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
|
||||
"When turned on it will be able to receive IPv6 traffic"),
|
||||
"ipv6InboundDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Allow IPv6 inbound"),
|
||||
"just": MessageLookupByLibrary.simpleMessage("Just"),
|
||||
"keepAliveIntervalDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
|
||||
@@ -267,6 +273,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"noHotKey": MessageLookupByLibrary.simpleMessage("No HotKey"),
|
||||
"noInfo": MessageLookupByLibrary.simpleMessage("No info"),
|
||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"),
|
||||
"noNetwork": MessageLookupByLibrary.simpleMessage("No network"),
|
||||
"noProxy": MessageLookupByLibrary.simpleMessage("No proxy"),
|
||||
"noProxyDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Please create a profile or add a valid profile"),
|
||||
@@ -393,7 +400,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"source": MessageLookupByLibrary.simpleMessage("Source"),
|
||||
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
||||
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
|
||||
"startVpn": MessageLookupByLibrary.simpleMessage("Starting VPN..."),
|
||||
"status": MessageLookupByLibrary.simpleMessage("Status"),
|
||||
"statusDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"System DNS will be used when turned off"),
|
||||
|
||||
@@ -120,6 +120,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
||||
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
|
||||
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
|
||||
"dnsResetTip": MessageLookupByLibrary.simpleMessage("确定重置DNS"),
|
||||
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
|
||||
"domain": MessageLookupByLibrary.simpleMessage("域名"),
|
||||
"download": MessageLookupByLibrary.simpleMessage("下载"),
|
||||
@@ -132,6 +133,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
||||
"expand": MessageLookupByLibrary.simpleMessage("标准"),
|
||||
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
|
||||
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
||||
"externalControllerDesc":
|
||||
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
|
||||
@@ -173,6 +176,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
|
||||
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
|
||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
|
||||
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
|
||||
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
||||
"keepAliveIntervalDesc":
|
||||
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
|
||||
@@ -213,6 +217,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
|
||||
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
|
||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
|
||||
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
|
||||
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
||||
"noProxyDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
|
||||
|
||||
@@ -1290,10 +1290,10 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Staring VPN...`
|
||||
/// `Starting VPN...`
|
||||
String get startVpn {
|
||||
return Intl.message(
|
||||
'Staring VPN...',
|
||||
'Starting VPN...',
|
||||
name: 'startVpn',
|
||||
desc: '',
|
||||
args: [],
|
||||
@@ -3099,6 +3099,56 @@ class AppLocalizations {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Make sure to reset the DNS`
|
||||
String get dnsResetTip {
|
||||
return Intl.message(
|
||||
'Make sure to reset the DNS',
|
||||
name: 'dnsResetTip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No network`
|
||||
String get noNetwork {
|
||||
return Intl.message(
|
||||
'No network',
|
||||
name: 'noNetwork',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Allow IPv6 inbound`
|
||||
String get ipv6InboundDesc {
|
||||
return Intl.message(
|
||||
'Allow IPv6 inbound',
|
||||
name: 'ipv6InboundDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Export logs`
|
||||
String get exportLogs {
|
||||
return Intl.message(
|
||||
'Export logs',
|
||||
name: 'exportLogs',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Export Success`
|
||||
String get exportSuccess {
|
||||
return Intl.message(
|
||||
'Export Success',
|
||||
name: 'exportSuccess',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/http.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/plugins/tile.dart';
|
||||
import 'package:fl_clash/plugins/vpn.dart';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -18,6 +18,7 @@ class AppStateManager extends StatefulWidget {
|
||||
|
||||
class _AppStateManagerState extends State<AppStateManager>
|
||||
with WidgetsBindingObserver {
|
||||
|
||||
_updateNavigationsContainer(Widget child) {
|
||||
return Selector2<AppState, Config, UpdateNavigationsSelector>(
|
||||
selector: (_, appState, config) {
|
||||
|
||||
@@ -19,9 +19,9 @@ class ClashManager extends StatefulWidget {
|
||||
State<ClashManager> createState() => _ClashContainerState();
|
||||
}
|
||||
|
||||
class _ClashContainerState extends State<ClashManager>
|
||||
with AppMessageListener {
|
||||
class _ClashContainerState extends State<ClashManager> with AppMessageListener {
|
||||
Function? updateClashConfigDebounce;
|
||||
Function? updateDelayDebounce;
|
||||
|
||||
Widget _updateContainer(Widget child) {
|
||||
return Selector2<Config, ClashConfig, ClashConfigState>(
|
||||
@@ -66,6 +66,7 @@ class _ClashContainerState extends State<ClashManager>
|
||||
selector: (_, config, clashConfig) => CoreState(
|
||||
accessControl: config.isAccessControl ? config.accessControl : null,
|
||||
enable: config.vpnProps.enable,
|
||||
ipv6: config.vpnProps.ipv6,
|
||||
allowBypass: config.vpnProps.allowBypass,
|
||||
systemProxy: config.vpnProps.systemProxy,
|
||||
mixedPort: clashConfig.mixedPort,
|
||||
@@ -132,12 +133,16 @@ class _ClashContainerState extends State<ClashManager>
|
||||
final appController = globalState.appController;
|
||||
appController.setDelay(delay);
|
||||
super.onDelay(delay);
|
||||
await globalState.appController.updateGroupDebounce();
|
||||
updateDelayDebounce ??= debounce(() async {
|
||||
await appController.updateGroupDebounce();
|
||||
await appController.addCheckIpNumDebounce();
|
||||
});
|
||||
updateDelayDebounce!();
|
||||
}
|
||||
|
||||
@override
|
||||
void onLog(Log log) {
|
||||
globalState.appController.appState.addLog(log);
|
||||
globalState.appController.appFlowingState.addLog(log);
|
||||
if (log.logLevel == LogLevel.error) {
|
||||
globalState.appController.showSnackBar(log.payload ?? '');
|
||||
}
|
||||
@@ -145,6 +150,12 @@ class _ClashContainerState extends State<ClashManager>
|
||||
super.onLog(log);
|
||||
}
|
||||
|
||||
@override
|
||||
void onStarted(String runTime) {
|
||||
super.onStarted(runTime);
|
||||
globalState.appController.applyProfileDebounce();
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(Connection connection) async {
|
||||
globalState.appController.appState.addRequest(connection);
|
||||
@@ -159,7 +170,7 @@ class _ClashContainerState extends State<ClashManager>
|
||||
providerName,
|
||||
),
|
||||
);
|
||||
appController.addCheckIpNumDebounce();
|
||||
// appController.addCheckIpNumDebounce();
|
||||
super.onLoaded(providerName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ class ProxyManager extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector3<AppState, Config, ClashConfig, ProxyState>(
|
||||
selector: (_, appState, config, clashConfig) => ProxyState(
|
||||
isStart: appState.isStart,
|
||||
return Selector3<AppFlowingState, Config, ClashConfig, ProxyState>(
|
||||
selector: (_, appFlowingState, config, clashConfig) => ProxyState(
|
||||
isStart: appFlowingState.isStart,
|
||||
systemProxy: config.desktopProps.systemProxy,
|
||||
port: clashConfig.mixedPort,
|
||||
),
|
||||
|
||||
@@ -42,7 +42,7 @@ class _TrayContainerState extends State<TrayManager> with TrayListener {
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
),
|
||||
);
|
||||
if(!Platform.isLinux){
|
||||
if (!Platform.isLinux) {
|
||||
await trayManager.setToolTip(
|
||||
appName,
|
||||
);
|
||||
@@ -138,11 +138,12 @@ class _TrayContainerState extends State<TrayManager> with TrayListener {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector3<AppState, Config, ClashConfig, TrayState>(
|
||||
selector: (_, appState, config, clashConfig) => TrayState(
|
||||
return Selector4<AppState, AppFlowingState, Config, ClashConfig, TrayState>(
|
||||
selector: (_, appState, appFlowingState, config, clashConfig) =>
|
||||
TrayState(
|
||||
mode: clashConfig.mode,
|
||||
autoLaunch: config.autoLaunch,
|
||||
isStart: appState.isStart,
|
||||
isStart: appFlowingState.isStart,
|
||||
locale: config.locale,
|
||||
systemProxy: config.desktopProps.systemProxy,
|
||||
tunEnable: clashConfig.tun.enable,
|
||||
|
||||
@@ -24,8 +24,8 @@ class _VpnContainerState extends State<VpnManager> {
|
||||
showTip() {
|
||||
vpnTipDebounce ??= debounce<Function()>(() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
if (appState.isStart) {
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
if (appFlowingState.isStart) {
|
||||
globalState.showSnackBar(
|
||||
context,
|
||||
message: appLocalizations.vpnTip,
|
||||
|
||||
@@ -12,12 +12,8 @@ typedef DelayMap = Map<String, int?>;
|
||||
|
||||
class AppState with ChangeNotifier {
|
||||
List<NavigationItem> _navigationItems;
|
||||
int? _runTime;
|
||||
bool _isInit;
|
||||
VersionInfo? _versionInfo;
|
||||
List<Traffic> _traffics;
|
||||
Traffic _totalTraffic;
|
||||
List<Log> _logs;
|
||||
String _currentLabel;
|
||||
SystemColorSchemes _systemColorSchemes;
|
||||
num _sortNum;
|
||||
@@ -42,16 +38,13 @@ class AppState with ChangeNotifier {
|
||||
}) : _navigationItems = [],
|
||||
_isInit = false,
|
||||
_currentLabel = "dashboard",
|
||||
_traffics = [],
|
||||
_logs = [],
|
||||
_viewWidth = 0,
|
||||
_viewWidth = other.getScreenSize().width,
|
||||
_selectedMap = selectedMap,
|
||||
_sortNum = 0,
|
||||
_checkIpNum = 0,
|
||||
_requests = [],
|
||||
_mode = mode,
|
||||
_brightness = null,
|
||||
_totalTraffic = Traffic(),
|
||||
_delayMap = {},
|
||||
_groups = [],
|
||||
_providers = [],
|
||||
@@ -101,17 +94,6 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool get isStart => _runTime != null;
|
||||
|
||||
int? get runTime => _runTime;
|
||||
|
||||
set runTime(int? value) {
|
||||
if (_runTime != value) {
|
||||
_runTime = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
String getDesc(String type, String proxyName) {
|
||||
final groupTypeNamesList = GroupType.values.map((e) => e.name).toList();
|
||||
if (!groupTypeNamesList.contains(type)) {
|
||||
@@ -158,33 +140,6 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
List<Traffic> get traffics => _traffics;
|
||||
|
||||
set traffics(List<Traffic> value) {
|
||||
if (_traffics != value) {
|
||||
_traffics = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -196,28 +151,8 @@ class AppState with ChangeNotifier {
|
||||
|
||||
addRequest(Connection value) {
|
||||
_requests = List.from(_requests)..add(value);
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
if (_requests.length > maxLength) {
|
||||
_requests = _requests.sublist(_requests.length - maxLength);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Log> get logs => _logs;
|
||||
|
||||
set logs(List<Log> value) {
|
||||
if (_logs != value) {
|
||||
_logs = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
addLog(Log log) {
|
||||
_logs = List.from(_logs)..add(log);
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
if (_logs.length > maxLength) {
|
||||
_logs = _logs.sublist(_logs.length - maxLength);
|
||||
}
|
||||
const maxLength = 1000;
|
||||
_requests = _requests.safeSublist(_requests.length - maxLength);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -383,3 +318,67 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppFlowingState with ChangeNotifier {
|
||||
int? _runTime;
|
||||
List<Log> _logs;
|
||||
List<Traffic> _traffics;
|
||||
Traffic _totalTraffic;
|
||||
|
||||
AppFlowingState()
|
||||
: _logs = [],
|
||||
_traffics = [],
|
||||
_totalTraffic = Traffic();
|
||||
|
||||
bool get isStart => _runTime != null;
|
||||
|
||||
int? get runTime => _runTime;
|
||||
|
||||
set runTime(int? value) {
|
||||
if (_runTime != value) {
|
||||
_runTime = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
List<Log> get logs => _logs;
|
||||
|
||||
set logs(List<Log> value) {
|
||||
if (_logs != value) {
|
||||
_logs = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
addLog(Log log) {
|
||||
_logs = List.from(_logs)..add(log);
|
||||
const maxLength = 1000;
|
||||
_logs = _logs.safeSublist(_logs.length - maxLength);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Traffic> get traffics => _traffics;
|
||||
|
||||
set traffics(List<Traffic> value) {
|
||||
if (_traffics != value) {
|
||||
_traffics = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
addTraffic(Traffic traffic) {
|
||||
_traffics = List.from(_traffics)..add(traffic);
|
||||
const maxLength = 60;
|
||||
_traffics = _traffics.safeSublist(_traffics.length - maxLength);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Traffic get totalTraffic => _totalTraffic;
|
||||
|
||||
set totalTraffic(Traffic value) {
|
||||
if (_totalTraffic != value) {
|
||||
_totalTraffic = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class Dns with _$Dns {
|
||||
@Default(false) @JsonKey(name: "prefer-h3") bool preferH3,
|
||||
@Default(true) @JsonKey(name: "use-hosts") bool useHosts,
|
||||
@Default(true) @JsonKey(name: "use-system-hosts") bool useSystemHosts,
|
||||
@Default(true) @JsonKey(name: "respect-rules") bool respectRules,
|
||||
@Default(false) @JsonKey(name: "respect-rules") bool respectRules,
|
||||
@Default(false) bool ipv6,
|
||||
@Default(["223.5.5.5"])
|
||||
@JsonKey(name: "default-nameserver")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -304,7 +303,7 @@ extension GroupExt on Group {
|
||||
String get realNow => now ?? "";
|
||||
|
||||
String getCurrentSelectedName(String proxyName) {
|
||||
if (type == GroupType.URLTest) {
|
||||
if (type.isURLTestOrFallback) {
|
||||
return realNow.isNotEmpty ? realNow : proxyName;
|
||||
}
|
||||
return proxyName.isNotEmpty ? proxyName : realNow;
|
||||
|
||||
@@ -42,6 +42,7 @@ class CoreState with _$CoreState {
|
||||
required bool allowBypass,
|
||||
required bool systemProxy,
|
||||
required int mixedPort,
|
||||
required bool ipv6,
|
||||
required bool onlyProxy,
|
||||
}) = _CoreState;
|
||||
|
||||
@@ -77,7 +78,8 @@ class WindowProps with _$WindowProps {
|
||||
class VpnProps with _$VpnProps {
|
||||
const factory VpnProps({
|
||||
@Default(true) bool enable,
|
||||
@Default(false) bool systemProxy,
|
||||
@Default(true) bool systemProxy,
|
||||
@Default(false) bool ipv6,
|
||||
@Default(true) bool allowBypass,
|
||||
}) = _VpnProps;
|
||||
|
||||
|
||||
@@ -772,7 +772,7 @@ class _$DnsImpl implements _Dns {
|
||||
@JsonKey(name: "prefer-h3") this.preferH3 = false,
|
||||
@JsonKey(name: "use-hosts") this.useHosts = true,
|
||||
@JsonKey(name: "use-system-hosts") this.useSystemHosts = true,
|
||||
@JsonKey(name: "respect-rules") this.respectRules = true,
|
||||
@JsonKey(name: "respect-rules") this.respectRules = false,
|
||||
this.ipv6 = false,
|
||||
@JsonKey(name: "default-nameserver")
|
||||
final List<String> defaultNameserver = const ["223.5.5.5"],
|
||||
|
||||
@@ -32,9 +32,9 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
|
||||
'mmdb':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
|
||||
'asn':
|
||||
'https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb',
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb',
|
||||
'geoip':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat',
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat',
|
||||
'geosite':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
|
||||
}
|
||||
@@ -141,7 +141,7 @@ _$DnsImpl _$$DnsImplFromJson(Map<String, dynamic> json) => _$DnsImpl(
|
||||
preferH3: json['prefer-h3'] as bool? ?? false,
|
||||
useHosts: json['use-hosts'] as bool? ?? true,
|
||||
useSystemHosts: json['use-system-hosts'] as bool? ?? true,
|
||||
respectRules: json['respect-rules'] as bool? ?? true,
|
||||
respectRules: json['respect-rules'] as bool? ?? false,
|
||||
ipv6: json['ipv6'] as bool? ?? false,
|
||||
defaultNameserver: (json['default-nameserver'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
|
||||
@@ -274,6 +274,7 @@ mixin _$CoreState {
|
||||
bool get allowBypass => throw _privateConstructorUsedError;
|
||||
bool get systemProxy => throw _privateConstructorUsedError;
|
||||
int get mixedPort => throw _privateConstructorUsedError;
|
||||
bool get ipv6 => throw _privateConstructorUsedError;
|
||||
bool get onlyProxy => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -294,6 +295,7 @@ abstract class $CoreStateCopyWith<$Res> {
|
||||
bool allowBypass,
|
||||
bool systemProxy,
|
||||
int mixedPort,
|
||||
bool ipv6,
|
||||
bool onlyProxy});
|
||||
|
||||
$AccessControlCopyWith<$Res>? get accessControl;
|
||||
@@ -318,6 +320,7 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
|
||||
Object? allowBypass = null,
|
||||
Object? systemProxy = null,
|
||||
Object? mixedPort = null,
|
||||
Object? ipv6 = null,
|
||||
Object? onlyProxy = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
@@ -345,6 +348,10 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
|
||||
? _value.mixedPort
|
||||
: mixedPort // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
@@ -380,6 +387,7 @@ abstract class _$$CoreStateImplCopyWith<$Res>
|
||||
bool allowBypass,
|
||||
bool systemProxy,
|
||||
int mixedPort,
|
||||
bool ipv6,
|
||||
bool onlyProxy});
|
||||
|
||||
@override
|
||||
@@ -403,6 +411,7 @@ class __$$CoreStateImplCopyWithImpl<$Res>
|
||||
Object? allowBypass = null,
|
||||
Object? systemProxy = null,
|
||||
Object? mixedPort = null,
|
||||
Object? ipv6 = null,
|
||||
Object? onlyProxy = null,
|
||||
}) {
|
||||
return _then(_$CoreStateImpl(
|
||||
@@ -430,6 +439,10 @@ class __$$CoreStateImplCopyWithImpl<$Res>
|
||||
? _value.mixedPort
|
||||
: mixedPort // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
@@ -448,6 +461,7 @@ class _$CoreStateImpl implements _CoreState {
|
||||
required this.allowBypass,
|
||||
required this.systemProxy,
|
||||
required this.mixedPort,
|
||||
required this.ipv6,
|
||||
required this.onlyProxy});
|
||||
|
||||
factory _$CoreStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -466,11 +480,13 @@ class _$CoreStateImpl implements _CoreState {
|
||||
@override
|
||||
final int mixedPort;
|
||||
@override
|
||||
final bool ipv6;
|
||||
@override
|
||||
final bool onlyProxy;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
|
||||
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, ipv6: $ipv6, onlyProxy: $onlyProxy)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -489,6 +505,7 @@ class _$CoreStateImpl implements _CoreState {
|
||||
other.systemProxy == systemProxy) &&
|
||||
(identical(other.mixedPort, mixedPort) ||
|
||||
other.mixedPort == mixedPort) &&
|
||||
(identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
|
||||
(identical(other.onlyProxy, onlyProxy) ||
|
||||
other.onlyProxy == onlyProxy));
|
||||
}
|
||||
@@ -503,6 +520,7 @@ class _$CoreStateImpl implements _CoreState {
|
||||
allowBypass,
|
||||
systemProxy,
|
||||
mixedPort,
|
||||
ipv6,
|
||||
onlyProxy);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@@ -527,6 +545,7 @@ abstract class _CoreState implements CoreState {
|
||||
required final bool allowBypass,
|
||||
required final bool systemProxy,
|
||||
required final int mixedPort,
|
||||
required final bool ipv6,
|
||||
required final bool onlyProxy}) = _$CoreStateImpl;
|
||||
|
||||
factory _CoreState.fromJson(Map<String, dynamic> json) =
|
||||
@@ -545,6 +564,8 @@ abstract class _CoreState implements CoreState {
|
||||
@override
|
||||
int get mixedPort;
|
||||
@override
|
||||
bool get ipv6;
|
||||
@override
|
||||
bool get onlyProxy;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
@@ -934,6 +955,7 @@ VpnProps _$VpnPropsFromJson(Map<String, dynamic> json) {
|
||||
mixin _$VpnProps {
|
||||
bool get enable => throw _privateConstructorUsedError;
|
||||
bool get systemProxy => throw _privateConstructorUsedError;
|
||||
bool get ipv6 => throw _privateConstructorUsedError;
|
||||
bool get allowBypass => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -947,7 +969,7 @@ abstract class $VpnPropsCopyWith<$Res> {
|
||||
factory $VpnPropsCopyWith(VpnProps value, $Res Function(VpnProps) then) =
|
||||
_$VpnPropsCopyWithImpl<$Res, VpnProps>;
|
||||
@useResult
|
||||
$Res call({bool enable, bool systemProxy, bool allowBypass});
|
||||
$Res call({bool enable, bool systemProxy, bool ipv6, bool allowBypass});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -965,6 +987,7 @@ class _$VpnPropsCopyWithImpl<$Res, $Val extends VpnProps>
|
||||
$Res call({
|
||||
Object? enable = null,
|
||||
Object? systemProxy = null,
|
||||
Object? ipv6 = null,
|
||||
Object? allowBypass = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
@@ -976,6 +999,10 @@ class _$VpnPropsCopyWithImpl<$Res, $Val extends VpnProps>
|
||||
? _value.systemProxy
|
||||
: systemProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
allowBypass: null == allowBypass
|
||||
? _value.allowBypass
|
||||
: allowBypass // ignore: cast_nullable_to_non_nullable
|
||||
@@ -992,7 +1019,7 @@ abstract class _$$VpnPropsImplCopyWith<$Res>
|
||||
__$$VpnPropsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({bool enable, bool systemProxy, bool allowBypass});
|
||||
$Res call({bool enable, bool systemProxy, bool ipv6, bool allowBypass});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -1008,6 +1035,7 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? enable = null,
|
||||
Object? systemProxy = null,
|
||||
Object? ipv6 = null,
|
||||
Object? allowBypass = null,
|
||||
}) {
|
||||
return _then(_$VpnPropsImpl(
|
||||
@@ -1019,6 +1047,10 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
|
||||
? _value.systemProxy
|
||||
: systemProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
allowBypass: null == allowBypass
|
||||
? _value.allowBypass
|
||||
: allowBypass // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1031,7 +1063,10 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$VpnPropsImpl implements _VpnProps {
|
||||
const _$VpnPropsImpl(
|
||||
{this.enable = true, this.systemProxy = false, this.allowBypass = true});
|
||||
{this.enable = true,
|
||||
this.systemProxy = true,
|
||||
this.ipv6 = false,
|
||||
this.allowBypass = true});
|
||||
|
||||
factory _$VpnPropsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$VpnPropsImplFromJson(json);
|
||||
@@ -1044,11 +1079,14 @@ class _$VpnPropsImpl implements _VpnProps {
|
||||
final bool systemProxy;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool ipv6;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool allowBypass;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VpnProps(enable: $enable, systemProxy: $systemProxy, allowBypass: $allowBypass)';
|
||||
return 'VpnProps(enable: $enable, systemProxy: $systemProxy, ipv6: $ipv6, allowBypass: $allowBypass)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1059,6 +1097,7 @@ class _$VpnPropsImpl implements _VpnProps {
|
||||
(identical(other.enable, enable) || other.enable == enable) &&
|
||||
(identical(other.systemProxy, systemProxy) ||
|
||||
other.systemProxy == systemProxy) &&
|
||||
(identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
|
||||
(identical(other.allowBypass, allowBypass) ||
|
||||
other.allowBypass == allowBypass));
|
||||
}
|
||||
@@ -1066,7 +1105,7 @@ class _$VpnPropsImpl implements _VpnProps {
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, enable, systemProxy, allowBypass);
|
||||
Object.hash(runtimeType, enable, systemProxy, ipv6, allowBypass);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@@ -1086,6 +1125,7 @@ abstract class _VpnProps implements VpnProps {
|
||||
const factory _VpnProps(
|
||||
{final bool enable,
|
||||
final bool systemProxy,
|
||||
final bool ipv6,
|
||||
final bool allowBypass}) = _$VpnPropsImpl;
|
||||
|
||||
factory _VpnProps.fromJson(Map<String, dynamic> json) =
|
||||
@@ -1096,6 +1136,8 @@ abstract class _VpnProps implements VpnProps {
|
||||
@override
|
||||
bool get systemProxy;
|
||||
@override
|
||||
bool get ipv6;
|
||||
@override
|
||||
bool get allowBypass;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
|
||||
@@ -176,6 +176,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
|
||||
allowBypass: json['allowBypass'] as bool,
|
||||
systemProxy: json['systemProxy'] as bool,
|
||||
mixedPort: (json['mixedPort'] as num).toInt(),
|
||||
ipv6: json['ipv6'] as bool,
|
||||
onlyProxy: json['onlyProxy'] as bool,
|
||||
);
|
||||
|
||||
@@ -187,6 +188,7 @@ Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
|
||||
'allowBypass': instance.allowBypass,
|
||||
'systemProxy': instance.systemProxy,
|
||||
'mixedPort': instance.mixedPort,
|
||||
'ipv6': instance.ipv6,
|
||||
'onlyProxy': instance.onlyProxy,
|
||||
};
|
||||
|
||||
@@ -224,7 +226,8 @@ Map<String, dynamic> _$$WindowPropsImplToJson(_$WindowPropsImpl instance) =>
|
||||
_$VpnPropsImpl _$$VpnPropsImplFromJson(Map<String, dynamic> json) =>
|
||||
_$VpnPropsImpl(
|
||||
enable: json['enable'] as bool? ?? true,
|
||||
systemProxy: json['systemProxy'] as bool? ?? false,
|
||||
systemProxy: json['systemProxy'] as bool? ?? true,
|
||||
ipv6: json['ipv6'] as bool? ?? false,
|
||||
allowBypass: json['allowBypass'] as bool? ?? true,
|
||||
);
|
||||
|
||||
@@ -232,6 +235,7 @@ Map<String, dynamic> _$$VpnPropsImplToJson(_$VpnPropsImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'enable': instance.enable,
|
||||
'systemProxy': instance.systemProxy,
|
||||
'ipv6': instance.ipv6,
|
||||
'allowBypass': instance.allowBypass,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -155,56 +154,47 @@ class HomePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BackScope(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
final appController = globalState.appController;
|
||||
final maxWidth = container.maxWidth;
|
||||
if (appController.appState.viewWidth != maxWidth) {
|
||||
globalState.appController.updateViewWidth(maxWidth);
|
||||
}
|
||||
return Selector2<AppState, Config, HomeState>(
|
||||
selector: (_, appState, config) {
|
||||
return HomeState(
|
||||
currentLabel: appState.currentLabel,
|
||||
navigationItems: appState.currentNavigationItems,
|
||||
viewMode: other.getViewMode(maxWidth),
|
||||
locale: config.locale,
|
||||
);
|
||||
},
|
||||
shouldRebuild: (prev, next) {
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, state, child) {
|
||||
final viewMode = state.viewMode;
|
||||
final navigationItems = state.navigationItems;
|
||||
final currentLabel = state.currentLabel;
|
||||
final index = navigationItems.lastIndexWhere(
|
||||
(element) => element.label == currentLabel,
|
||||
);
|
||||
final currentIndex = index == -1 ? 0 : index;
|
||||
final navigationBar = _getNavigationBar(
|
||||
context: context,
|
||||
viewMode: viewMode,
|
||||
navigationItems: navigationItems,
|
||||
currentIndex: currentIndex,
|
||||
);
|
||||
final bottomNavigationBar =
|
||||
viewMode == ViewMode.mobile ? navigationBar : null;
|
||||
final sideNavigationBar =
|
||||
viewMode != ViewMode.mobile ? navigationBar : null;
|
||||
return CommonScaffold(
|
||||
key: globalState.homeScaffoldKey,
|
||||
title: Intl.message(
|
||||
currentLabel,
|
||||
),
|
||||
sideNavigationBar: sideNavigationBar,
|
||||
body: child!,
|
||||
bottomNavigationBar: bottomNavigationBar,
|
||||
);
|
||||
},
|
||||
child: _buildPageView(),
|
||||
child: Selector2<AppState, Config, HomeState>(
|
||||
selector: (_, appState, config) {
|
||||
return HomeState(
|
||||
currentLabel: appState.currentLabel,
|
||||
navigationItems: appState.currentNavigationItems,
|
||||
viewMode: appState.viewMode,
|
||||
locale: config.locale,
|
||||
);
|
||||
},
|
||||
shouldRebuild: (prev, next) {
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, state, child) {
|
||||
final viewMode = state.viewMode;
|
||||
final navigationItems = state.navigationItems;
|
||||
final currentLabel = state.currentLabel;
|
||||
final index = navigationItems.lastIndexWhere(
|
||||
(element) => element.label == currentLabel,
|
||||
);
|
||||
final currentIndex = index == -1 ? 0 : index;
|
||||
final navigationBar = _getNavigationBar(
|
||||
context: context,
|
||||
viewMode: viewMode,
|
||||
navigationItems: navigationItems,
|
||||
currentIndex: currentIndex,
|
||||
);
|
||||
final bottomNavigationBar =
|
||||
viewMode == ViewMode.mobile ? navigationBar : null;
|
||||
final sideNavigationBar =
|
||||
viewMode != ViewMode.mobile ? navigationBar : null;
|
||||
return CommonScaffold(
|
||||
key: globalState.homeScaffoldKey,
|
||||
title: Intl.message(
|
||||
currentLabel,
|
||||
),
|
||||
sideNavigationBar: sideNavigationBar,
|
||||
body: child!,
|
||||
bottomNavigationBar: bottomNavigationBar,
|
||||
);
|
||||
},
|
||||
child: _buildPageView(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +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:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Service {
|
||||
@@ -28,4 +27,4 @@ class Service {
|
||||
}
|
||||
|
||||
final service =
|
||||
Platform.isAndroid && !globalState.isVpnService ? Service() : null;
|
||||
Platform.isAndroid ? Service() : null;
|
||||
|
||||
@@ -6,8 +6,6 @@ import 'dart:isolate';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Vpn {
|
||||
@@ -105,4 +103,4 @@ class Vpn {
|
||||
}
|
||||
}
|
||||
|
||||
final vpn = Platform.isAndroid && globalState.isVpnService ? Vpn() : null;
|
||||
final vpn = Platform.isAndroid ? Vpn() : null;
|
||||
|
||||
@@ -76,7 +76,7 @@ class GlobalState {
|
||||
required ClashConfig clashConfig,
|
||||
}) async {
|
||||
clashCore.start();
|
||||
if (vpn != null) {
|
||||
if (globalState.isVpnService) {
|
||||
await vpn?.startVpn(clashConfig.mixedPort);
|
||||
startListenUpdate();
|
||||
return;
|
||||
@@ -90,7 +90,7 @@ class GlobalState {
|
||||
startTime = clashCore.getRunTime();
|
||||
}
|
||||
|
||||
handleStop() async {
|
||||
Future handleStop() async {
|
||||
clashCore.stop();
|
||||
if (Platform.isAndroid) {
|
||||
clashCore.stopTun();
|
||||
@@ -133,6 +133,7 @@ class GlobalState {
|
||||
CoreState(
|
||||
enable: config.vpnProps.enable,
|
||||
accessControl: config.isAccessControl ? config.accessControl : null,
|
||||
ipv6: config.vpnProps.ipv6,
|
||||
allowBypass: config.vpnProps.allowBypass,
|
||||
systemProxy: config.vpnProps.systemProxy,
|
||||
mixedPort: clashConfig.mixedPort,
|
||||
@@ -222,7 +223,7 @@ class GlobalState {
|
||||
}
|
||||
|
||||
updateTraffic({
|
||||
AppState? appState,
|
||||
AppFlowingState? appFlowingState,
|
||||
}) {
|
||||
final traffic = clashCore.getTraffic();
|
||||
if (Platform.isAndroid && isVpnService == true) {
|
||||
@@ -231,9 +232,9 @@ class GlobalState {
|
||||
content: "$traffic",
|
||||
);
|
||||
} else {
|
||||
if (appState != null) {
|
||||
appState.addTraffic(traffic);
|
||||
appState.totalTraffic = clashCore.getTotalTraffic();
|
||||
if (appFlowingState != null) {
|
||||
appFlowingState.addTraffic(traffic);
|
||||
appFlowingState.totalTraffic = clashCore.getTotalTraffic();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,7 +244,7 @@ class GlobalState {
|
||||
required String message,
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
final width = context.width;
|
||||
final width = context.viewWidth;
|
||||
EdgeInsets margin;
|
||||
if (width < 600) {
|
||||
margin = const EdgeInsets.only(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'text.dart';
|
||||
|
||||
@@ -149,7 +149,6 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
final Widget Function(T item)? subtitleBuilder;
|
||||
final Function(T item) onAdd;
|
||||
final Function(T item) onRemove;
|
||||
final bool isMap;
|
||||
|
||||
const UpdatePage({
|
||||
super.key,
|
||||
@@ -158,10 +157,37 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
required this.titleBuilder,
|
||||
required this.onRemove,
|
||||
required this.onAdd,
|
||||
this.isMap = false,
|
||||
this.subtitleBuilder,
|
||||
});
|
||||
|
||||
bool get isMap => items is Iterable<MapEntry>;
|
||||
|
||||
_handleEdit(T item) async {
|
||||
if (isMap) {
|
||||
item as MapEntry<String, String>;
|
||||
final value = await globalState.showCommonDialog<T>(
|
||||
child: AddDialog(
|
||||
defaultKey: item.key,
|
||||
defaultValue: item.value,
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
if (value == null) return;
|
||||
onAdd(value);
|
||||
} else {
|
||||
item as String;
|
||||
final value = await globalState.showCommonDialog<T>(
|
||||
child: AddDialog(
|
||||
defaultKey: null,
|
||||
defaultValue: item,
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
if (value == null) return;
|
||||
onAdd(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatLayout(
|
||||
@@ -170,7 +196,8 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
onPressed: () async {
|
||||
final value = await globalState.showCommonDialog<T>(
|
||||
child: AddDialog(
|
||||
isMap: isMap,
|
||||
defaultKey: isMap ? "" : null,
|
||||
defaultValue: "",
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
@@ -202,7 +229,9 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
_handleEdit(e);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -213,12 +242,14 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
|
||||
class AddDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final bool isMap;
|
||||
final String? defaultKey;
|
||||
final String defaultValue;
|
||||
|
||||
const AddDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.isMap,
|
||||
this.defaultKey,
|
||||
required this.defaultValue,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -233,13 +264,19 @@ class _AddDialogState extends State<AddDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
keyController = TextEditingController();
|
||||
valueController = TextEditingController();
|
||||
keyController = TextEditingController(
|
||||
text: widget.defaultKey,
|
||||
);
|
||||
valueController = TextEditingController(
|
||||
text: widget.defaultValue,
|
||||
);
|
||||
}
|
||||
|
||||
bool get hasKey => widget.defaultKey != null;
|
||||
|
||||
_submit() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
if (widget.isMap) {
|
||||
if (hasKey) {
|
||||
Navigator.of(context).pop<MapEntry<String, String>>(
|
||||
MapEntry(
|
||||
keyController.text,
|
||||
@@ -264,7 +301,7 @@ class _AddDialogState extends State<AddDialog> {
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
if (widget.isMap)
|
||||
if (hasKey)
|
||||
TextFormField(
|
||||
maxLines: 2,
|
||||
minLines: 1,
|
||||
|
||||
@@ -462,7 +462,6 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
);
|
||||
TweenSequence<Color?>? colorTween;
|
||||
TweenSequence<double>? closedOpacityTween, openOpacityTween;
|
||||
Animatable<Color?>? scrimTween;
|
||||
switch (animation.status) {
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.forward:
|
||||
|
||||
@@ -53,7 +53,7 @@ showExtendPage(
|
||||
body: uniqueBody,
|
||||
);
|
||||
return SizedBox(
|
||||
width: isMobile ? context.width : extendPageWidth ?? 300,
|
||||
width: isMobile ? context.viewWidth : extendPageWidth ?? 300,
|
||||
child: commonScaffold,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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.60+202409171
|
||||
version: 0.8.62+202409261
|
||||
environment:
|
||||
sdk: '>=3.1.0 <4.0.0'
|
||||
flutter: 3.22.0
|
||||
|
||||
28
setup.dart
28
setup.dart
@@ -55,7 +55,8 @@ class BuildLibItem {
|
||||
}
|
||||
|
||||
class Build {
|
||||
static List<BuildLibItem> get buildItems => [
|
||||
static List<BuildLibItem> get buildItems =>
|
||||
[
|
||||
BuildLibItem(
|
||||
platform: PlatformType.macos,
|
||||
arch: Arch.amd64,
|
||||
@@ -114,7 +115,7 @@ class Build {
|
||||
final ndk = environment["ANDROID_NDK"];
|
||||
assert(ndk != null);
|
||||
final prebuiltDir =
|
||||
Directory(join(ndk!, "toolchains", "llvm", "prebuilt"));
|
||||
Directory(join(ndk!, "toolchains", "llvm", "prebuilt"));
|
||||
final prebuiltDirList = prebuiltDir.listSync();
|
||||
final map = {
|
||||
"armeabi-v7a": "armv7a-linux-androideabi21-clang",
|
||||
@@ -133,8 +134,7 @@ class Build {
|
||||
|
||||
static get tags => "with_gvisor";
|
||||
|
||||
static Future<void> exec(
|
||||
List<String> executable, {
|
||||
static Future<void> exec(List<String> executable, {
|
||||
String? name,
|
||||
Map<String, String>? environment,
|
||||
String? workingDirectory,
|
||||
@@ -163,7 +163,7 @@ class Build {
|
||||
Arch? arch,
|
||||
}) async {
|
||||
final items = buildItems.where(
|
||||
(element) {
|
||||
(element) {
|
||||
return element.platform == platform &&
|
||||
(arch == null ? true : element.arch == arch);
|
||||
},
|
||||
@@ -187,6 +187,7 @@ class Build {
|
||||
env["GOARCH"] = item.arch.name;
|
||||
env["CGO_ENABLED"] = "1";
|
||||
env["CC"] = _getCc(item);
|
||||
env["CFLAGS"] = "-O3 -Werror";
|
||||
|
||||
await exec(
|
||||
[
|
||||
@@ -275,10 +276,11 @@ class BuildCommand extends Command {
|
||||
@override
|
||||
String get name => platform.name;
|
||||
|
||||
List<Arch> get arches => Build.buildItems
|
||||
.where((element) => element.platform == platform)
|
||||
.map((e) => e.arch)
|
||||
.toList();
|
||||
List<Arch> get arches =>
|
||||
Build.buildItems
|
||||
.where((element) => element.platform == platform)
|
||||
.map((e) => e.arch)
|
||||
.toList();
|
||||
|
||||
Future<void> _buildLib(Arch? arch) async {
|
||||
await Build.buildLib(platform: platform, arch: arch);
|
||||
@@ -338,7 +340,8 @@ class BuildCommand extends Command {
|
||||
await Build.exec(
|
||||
name: name,
|
||||
Build.getExecutable(
|
||||
"flutter_distributor package --skip-clean --platform ${platform.name} --targets $targets --flutter-build-args=verbose $args",
|
||||
"flutter_distributor package --skip-clean --platform ${platform
|
||||
.name} --targets $targets --flutter-build-args=verbose $args",
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -348,7 +351,7 @@ class BuildCommand extends Command {
|
||||
final String build = argResults?['build'] ?? 'all';
|
||||
final archName = argResults?['arch'];
|
||||
final currentArches =
|
||||
arches.where((element) => element.name == archName).toList();
|
||||
arches.where((element) => element.name == archName).toList();
|
||||
final arch = currentArches.isEmpty ? null : currentArches.first;
|
||||
if (arch == null && platform == PlatformType.windows) {
|
||||
throw "Invalid arch";
|
||||
@@ -386,7 +389,8 @@ class BuildCommand extends Command {
|
||||
platform: platform,
|
||||
targets: "apk",
|
||||
args:
|
||||
"--flutter-build-args split-per-abi --build-target-platform ${defaultTargets.join(",")}",
|
||||
"--flutter-build-args split-per-abi --build-target-platform ${defaultTargets
|
||||
.join(",")}",
|
||||
);
|
||||
case PlatformType.macos:
|
||||
await _getMacosDependencies();
|
||||
|
||||
@@ -2,24 +2,15 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/other.dart';
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
|
||||
void main() {
|
||||
final res = [1, 2, 3, 4, 5, 6,7,8,9,10,11];
|
||||
|
||||
print(res.batch(5));
|
||||
startService();
|
||||
}
|
||||
|
||||
startService() async {
|
||||
// 定义服务器将要监听的地址和端口
|
||||
final host = InternetAddress.anyIPv4; // 监听所有网络接口
|
||||
const port = 8080; // 使用 8080 端口
|
||||
|
||||
try {
|
||||
// 创建服务器
|
||||
final server = await HttpServer.bind(host, port);
|
||||
final server = await HttpServer.bind("127.0.0.1", 10001);
|
||||
print('服务器正在监听 ${server.address.address}:${server.port}');
|
||||
|
||||
// 监听请求
|
||||
|
||||
Reference in New Issue
Block a user