Compare commits

...

6 Commits

Author SHA1 Message Date
chen08209
23e3baf534 Optimize ip detection
Support android vpn ipv6 inbound switch

Support log export

Optimize more details
2024-09-26 14:27:52 +08:00
chen08209
d522a64604 Fix android system dns issues
Optimize dns default option

Fix some issues
2024-09-20 11:21:25 +08:00
chen08209
3902ea0064 Update readme 2024-09-18 00:42:20 +08:00
chen08209
5566f2b54f Update README.md 2 2024-09-18 00:22:31 +08:00
chen08209
511b7c521a Update README.md 2 2024-09-18 00:20:00 +08:00
chen08209
fb01d87371 Update README.md 2024-09-18 00:17:12 +08:00
84 changed files with 35176 additions and 30460 deletions

View File

@@ -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>
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](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

View File

@@ -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>
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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?
}

View File

@@ -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())

View File

@@ -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()
}
}

View File

@@ -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 {

View File

@@ -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()

View File

@@ -16,6 +16,7 @@ data class Props(
val accessControl: AccessControl?,
val allowBypass: Boolean?,
val systemProxy: Boolean?,
val ipv6: Boolean?,
)
data class TunProps(

View File

@@ -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

View File

@@ -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()

View File

@@ -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()
}
}

View File

@@ -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.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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();
};
}
}

View File

@@ -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';

View File

@@ -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"
};

View File

@@ -11,7 +11,7 @@ extension BuildContextExtension on BuildContext {
return MediaQuery.of(this).size;
}
double get width {
double get viewWidth {
return appSize.width;
}

View File

@@ -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";
};

View File

@@ -62,6 +62,6 @@ extension DoubleListExt on List<double> {
}
}
return -1; // 这行理论上不会执行到,但为了完整性保留
return -1;
}
}

View File

@@ -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);
}
}

View File

@@ -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
View 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;
}
}

View File

@@ -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();

View File

@@ -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';

View File

@@ -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,

View File

@@ -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;

View File

@@ -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';

View File

@@ -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,

View File

@@ -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,
);
}
}

View File

@@ -226,7 +226,6 @@ class HostsItem extends StatelessWidget {
clashConfig.hosts = Map.from(clashConfig.hosts)
..addEntries([value]);
},
isMap: true,
);
},
),

View File

@@ -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(),
],
),
),

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:math';
import 'package:fl_clash/common/common.dart';

View File

@@ -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,
),

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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(

View File

@@ -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;

View File

@@ -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(

View File

@@ -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(

View File

@@ -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);
},
);
}

View File

@@ -487,7 +487,7 @@ class _ListHeaderState extends State<ListHeader>
),
Flexible(
flex: 1,
child: currentGroupProxyNameBuilder(
child: currentSelectedProxyNameBuilder(
groupName: groupName,
builder: (currentGroupName) {
return Row(

View File

@@ -321,7 +321,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
top: 16,
left: 16,
right: 16,
bottom: 80,
bottom: 96,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,

View File

@@ -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,

View File

@@ -70,7 +70,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
final isDisclaimerAccepted =
await globalState.appController.showDisclaimer();
if (!isDisclaimerAccepted) {
system.exit();
globalState.appController.handleExit();
}
},
),

View File

@@ -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"
}

View File

@@ -303,5 +303,10 @@
"inputCorrectHotkey": "请输入正确的快捷键",
"hotkeyConflict": "快捷键冲突",
"remove": "移除",
"noHotKey": "暂无快捷键"
"noHotKey": "暂无快捷键",
"dnsResetTip": "确定重置DNS",
"noNetwork": "无网络",
"ipv6InboundDesc": "允许IPv6入站",
"exportLogs": "导出日志",
"exportSuccess": "导出成功"
}

View File

@@ -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"),

View File

@@ -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("请创建配置文件或者添加有效配置文件"),

View File

@@ -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> {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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,
),

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();
}
}
}

View File

@@ -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")

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"],

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,
};

View File

@@ -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(),
),
);
}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(

View File

@@ -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';

View File

@@ -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,

View File

@@ -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:

View File

@@ -53,7 +53,7 @@ showExtendPage(
body: uniqueBody,
);
return SizedBox(
width: isMobile ? context.width : extendPageWidth ?? 300,
width: isMobile ? context.viewWidth : extendPageWidth ?? 300,
child: commonScaffold,
);
},

View File

@@ -1,7 +1,7 @@
name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none'
version: 0.8.60+202409171
version: 0.8.62+202409261
environment:
sdk: '>=3.1.0 <4.0.0'
flutter: 3.22.0

View File

@@ -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();

View File

@@ -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}');
// 监听请求