Compare commits

..

71 Commits

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

Add access all selected button

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

Adapt LoadBalance and Relay

Add arm

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

Optimize more detail
2024-06-05 13:44:11 +08:00
chen08209
01f1b2d72f Add access selected sorted 2024-06-05 13:43:44 +08:00
chen08209
3074b1299e Fix notification duplicate creation issue
Fix AccessControl click issue
2024-06-03 11:48:50 +08:00
chen08209
bd5470b863 Fix Workflow 2024-05-31 22:34:46 +08:00
chen08209
83fafa4b68 Fix Linux unable to open 2024-05-31 22:12:41 +08:00
chen08209
b7fb969301 Update README.md 3 2024-05-31 15:41:08 +08:00
chen08209
3ef3190785 Create LICENSE 2024-05-31 15:11:15 +08:00
chen08209
b1c763fcfa Update README.md 2 2024-05-31 15:08:51 +08:00
chen08209
9452ffa514 Update README.md 2024-05-31 15:02:18 +08:00
chen08209
c9291e5027 Optimize workFlow 2024-05-31 14:25:15 +08:00
chen08209
3ba86dc9c2 optimize checkUpdate 2024-05-31 10:07:51 +08:00
chen08209
fd3040283c Fix submit error 2024-05-30 17:22:23 +08:00
chen08209
91d30c0f0e add WebDAV
add Auto check updates

Optimize more details
2024-05-30 17:11:15 +08:00
chen08209
c4b470ffaf optimize delayTest 2024-05-17 19:54:57 +08:00
chen08209
9a07c785f2 upgrade flutter version 2024-05-15 20:34:59 +08:00
chen08209
a134c32493 Update kernel
Add import profile via QR code image
2024-05-15 20:21:02 +08:00
chen08209
472cea9037 Add compatibility mode and adapt clash scheme. 2024-05-11 14:10:06 +08:00
chen08209
08d07498b9 update Version 2024-05-07 18:32:21 +08:00
chen08209
d5aa09949a Reconstruction application proxy logic 2024-05-07 18:31:14 +08:00
chen08209
fd1dfe5c60 Fix Tab destroy error 2024-05-06 19:05:27 +08:00
chen08209
9f89fe8b29 Optimize repeat healthcheck 2024-05-06 17:17:26 +08:00
chen08209
78081a12e8 Optimize Direct mode ui 2024-05-06 15:27:37 +08:00
chen08209
6896837f28 Optimize Healthcheck 2024-05-06 14:32:23 +08:00
chen08209
85eb903402 Remove proxies position animation, improve performance
Add Telegram Link
2024-05-06 14:32:22 +08:00
chen08209
9aa9180f1f Update healthcheck policy 2024-05-06 14:32:21 +08:00
chen08209
feb9688a29 New Check URLTest 2024-05-06 14:32:21 +08:00
chen08209
5c71992174 Fix the problem of invalid auto-selection 2024-05-05 16:14:34 +08:00
chen08209
74c3d0ae25 New Async UpdateConfig 2024-05-05 03:13:52 +08:00
chen08209
ecd1bcafd5 add changeProfileDebounce 2024-05-05 03:13:51 +08:00
chen08209
184d2d117a Update Workflow 2024-05-05 03:13:50 +08:00
chen08209
89e6f17794 Fix ChangeProfile block 2024-05-05 03:13:49 +08:00
chen08209
aef50fe0e3 Fix Release Message Error 2024-05-04 16:14:03 +08:00
chen08209
fc0767ed25 Update Selector 2 2024-05-04 01:14:56 +08:00
chen08209
dbf1724cca Update Version 2024-05-04 00:14:34 +08:00
chen08209
909aa4038e Fix Proxies Select Error 2024-05-04 00:14:07 +08:00
chen08209
2d0a7d8d46 Fix the problem that the proxy group is empty in global mode. 2024-05-03 23:08:06 +08:00
chen08209
ca96cd1d82 Fix the problem that the proxy group is empty in global mode. 2024-05-03 23:07:38 +08:00
chen08209
91ab1e5dac Add ProxyProvider2 2024-05-03 21:48:22 +08:00
chen08209
b3a5f74df8 Add ProxyProvider 2024-05-03 21:28:22 +08:00
chen08209
1f98be8ad8 Update Version 2024-05-03 15:33:46 +08:00
chen08209
453c7c98d0 Update ProxyGroup Sort 2024-05-03 15:33:45 +08:00
chen08209
91faed35c0 Fix Android quickStart VpnService some problems 2024-05-02 00:32:11 +08:00
chen08209
07bbaf6b6f Update version 2024-05-01 23:40:04 +08:00
chen08209
e8feb7c431 Set Android notification low importance 2024-05-01 23:40:03 +08:00
chen08209
4d16820526 Fix the issue that VpnService can't be closed correctly in special cases 2024-05-01 23:40:00 +08:00
chen08209
92294b49c6 Fix the problem that TileService is not destroyed correctly in some cases
Adjust tab animation defaults
2024-05-01 23:39:59 +08:00
chen08209
8a188a37c9 Add Telegram in README_zh_CN.md 2024-05-01 21:52:07 +08:00
chen08209
48af16c265 Add Telegram 2024-05-01 21:50:26 +08:00
54 changed files with 1017 additions and 431 deletions

View File

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

2
.gitmodules vendored
View File

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

View File

@@ -34,6 +34,8 @@ on Mobile:
💡 Based on Material You Design, [Surfboard](https://github.com/getsurfboard/surfboard)-like UI
☁️ Supports data sync via WebDAV
✨ Support subscription link, Dark mode
## Contact

View File

@@ -34,6 +34,8 @@ on Mobile:
💡 基本 Material You 设计, 类[Surfboard](https://github.com/getsurfboard/surfboard)用户界面
☁️ 支持通过WebDAV同步数据
✨ 支持一键导入订阅, 深色模式
## Contact

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

View File

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

View File

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

View File

@@ -321,12 +321,12 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig, compatible bool) {
targetConfig.ExternalController = ""
targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = ""
targetConfig.Interface = ""
targetConfig.ExternalUIURL = ""
targetConfig.GeodataMode = false
//targetConfig.IPv6 = patchConfig.IPv6
targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
targetConfig.SocksPort = 0
@@ -336,8 +336,8 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Mode = patchConfig.Mode
targetConfig.Tun.Enable = patchConfig.Tun.Enable
targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack
//targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
//targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.GeodataLoader = "standard"
targetConfig.Profile.StoreSelected = false
if targetConfig.DNS.Enable == false {

View File

@@ -1,6 +1,6 @@
module core
go 1.20
go 1.21.0
replace github.com/metacubex/mihomo => ./Clash.Meta

View File

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

View File

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

View File

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

View File

@@ -256,6 +256,29 @@ class AppController {
}
}
init() async {
if (!config.silentLaunch) {
window?.show();
}
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if(commonScaffoldState?.mounted == true){
await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
});
}else{
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
await afterInit();
}
afterInit() async {
if (config.autoRun) {
await updateSystemProxy(true);
@@ -265,9 +288,6 @@ class AppController {
}
autoUpdateProfiles();
updateLogStatus();
if (!config.silentLaunch) {
window?.show();
}
autoCheckUpdate();
}

View File

@@ -1,6 +1,6 @@
// ignore_for_file: constant_identifier_names
enum GroupType { Selector, URLTest, Fallback }
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
@@ -61,4 +61,4 @@ enum MessageType { log, tun, delay, process, now }
enum RecoveryOption {
all,
onlyProfiles,
}
}

View File

@@ -8,6 +8,13 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
extension AccessControlExtension on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
class AccessFragment extends StatefulWidget {
const AccessFragment({super.key});
@@ -83,136 +90,70 @@ class _AccessFragmentState extends State<AccessFragment> {
);
}
Widget _buildSelectedAllButton({
required bool isSelectedAll,
required List<String> allValueList,
}) {
return Builder(
builder: (context) {
final tooltip = isSelectedAll
? appLocalizations.cancelSelectAll
: appLocalizations.selectAll;
return IconButton(
tooltip: tooltip,
onPressed: () {
final config = globalState.appController.config;
final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: [],
),
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
icon: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
);
Widget _buildSearchButton(List<Package> packages) {
return IconButton(
tooltip: appLocalizations.search,
onPressed: () {
showSearch(
context: context,
delegate: AccessControlSearchDelegate(
packages: packages,
),
).then((_) => {setState(() {})});
},
icon: const Icon(Icons.search),
);
}
Widget _actionHeader({
_buildSelectedAllButton({
required bool isAccessControl,
required List<String> valueList,
required String describe,
required List<String> packageNameList,
required bool isSelectedAll,
required List<String> allValueList,
}) {
return AbsorbPointer(
absorbing: !isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color:
Theme.of(context).colorScheme.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color:
Theme.of(context).colorScheme.primary,
),
),
),
],
),
WidgetsBinding.instance.addPostFrameCallback((_) {
final tooltip = isSelectedAll
? appLocalizations.cancelSelectAll
: appLocalizations.selectAll;
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DisabledMask(
status: !isAccessControl,
child: AbsorbPointer(
absorbing: !isAccessControl,
child: FloatingActionButton (
tooltip: tooltip,
onPressed: () {
final config = globalState.appController.config;
final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: [],
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSelectedAllButton(
isSelectedAll: const ListEquality<String>()
.equals(valueList, packageNameList),
allValueList: packageNameList,
),
),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
child: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
),
),
),
);
);
});
}
Widget _buildPackageList() {
@@ -252,14 +193,11 @@ class _AccessFragmentState extends State<AccessFragment> {
accessControlMode == AccessControlMode.acceptSelected
? acceptPackages
: rejectPackages;
final currentList =
accessControlMode == AccessControlMode.acceptSelected
? accessControl.acceptList
: accessControl.rejectList;
final currentList = accessControl.currentList;
final currentPackages = isFilterSystemApp
? packages
.where((element) => element.isSystem == false)
.toList()
.where((element) => element.isSystem == false)
.toList()
: packages;
final packageNameList =
currentPackages.map((e) => e.packageName).toList();
@@ -268,15 +206,91 @@ class _AccessFragmentState extends State<AccessFragment> {
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
_buildSelectedAllButton(
isAccessControl: isAccessControl,
isSelectedAll: valueList.length == packageNameList.length,
allValueList: packageNameList,
);
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
_actionHeader(
isAccessControl: isAccessControl,
valueList: valueList,
describe: describe,
packageNameList: packageNameList,
AbsorbPointer(
absorbing: !isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(currentPackages)),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
),
),
),
Expanded(
flex: 1,
@@ -429,3 +443,111 @@ class PackageListItem extends StatelessWidget {
);
}
}
class AccessControlSearchDelegate extends SearchDelegate {
final List<Package> packages;
AccessControlSearchDelegate({
required this.packages,
});
List<Package> get _results {
final lowQuery = query.toLowerCase();
return packages
.where(
(package) =>
package.label.toLowerCase().contains(lowQuery) ||
package.packageName.contains(lowQuery),
)
.toList();
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
onPressed: () {
if (query.isEmpty) {
close(context, null);
return;
}
query = '';
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: const Icon(Icons.arrow_back),
);
}
Widget _packageList(List<Package> packages) {
return Selector<Config, PackageListSelectorState>(
selector: (_, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final currentList = accessControl.currentList;
final packageNameList =
this.packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
return DisabledMask(
status: !isAccessControl,
child: ListView.builder(
itemCount: packages.length,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config = globalState.appController.config;
if (accessControlMode == AccessControlMode.acceptSelected) {
config.accessControl = config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl = config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
);
},
);
}
@override
Widget buildResults(BuildContext context) {
return buildSuggestions(context);
}
@override
Widget buildSuggestions(BuildContext context) {
return _packageList(_results);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
@@ -54,8 +56,8 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onTab: () {
_modifyMixedPort(mixedPort);
},
padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 4),
leading: const Icon(Icons.adjust),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
trailing: FilledButton.tonal(
onPressed: () {
@@ -68,6 +70,24 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("Ipv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.allowLan,
builder: (_, allowLan, __) {
@@ -105,11 +125,29 @@ class _ConfigFragmentState extends State<ConfigFragment> {
// );
// },
// ),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow),
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
return ListItem.switchItem(
leading: const Icon(Icons.expand),
leading: const Icon(Icons.expand_outlined),
title: Text(appLocalizations.compatible),
subtitle: Text(appLocalizations.compatibleDesc),
delegate: SwitchDelegate(
@@ -125,25 +163,39 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
// Selector<ClashConfig, bool>(
// selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
// builder: (_, hasExternalController, __) {
// return ListItem.switchItem(
// leading: const Icon(Icons.api_outlined),
// title: Text(appLocalizations.externalController),
// subtitle: Text(appLocalizations.externalControllerDesc),
// delegate: SwitchDelegate(
// value: hasExternalController,
// onChanged: (bool value) async {
// final appController = globalState.appController;
// appController.clashConfig.externalController =
// value ? defaultExternalController : '';
// await appController.updateClashConfig(
// isPatch: false,
// );
// },
// ),
// );
// },
// ),
Padding(
padding: kMaterialListPadding,
child: Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.feedback),
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
trailing: SizedBox(
height: 48,
child: DropdownMenu<LogLevel>(
width: 124,
inputDecorationTheme: const InputDecorationTheme(
filled: true,
contentPadding: EdgeInsets.symmetric(
vertical: 5,
horizontal: 16,
),
),
initialSelection: value,
dropdownMenuEntries: [
for (final logLevel in LogLevel.values)

View File

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

View File

@@ -17,9 +17,16 @@ enum ProfileActions {
delete,
}
class ProfilesFragment extends StatelessWidget {
class ProfilesFragment extends StatefulWidget {
const ProfilesFragment({super.key});
@override
State<ProfilesFragment> createState() => _ProfilesFragmentState();
}
class _ProfilesFragmentState extends State<ProfilesFragment> {
final hasPadding = ValueNotifier<bool>(false);
_handleShowAddExtendPage() {
showExtendPage(
globalState.navigatorKey.currentState!.context,
@@ -41,7 +48,7 @@ class ProfilesFragment extends StatelessWidget {
}
}
_initScaffoldState(BuildContext context) {
_initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
@@ -55,7 +62,7 @@ class ProfilesFragment extends StatelessWidget {
},
);
},
icon: const Icon(Icons.download),
icon: const Icon(Icons.sync),
),
const SizedBox(
width: 8,
@@ -78,7 +85,7 @@ class ProfilesFragment extends StatelessWidget {
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState(context);
_initScaffoldState();
}
return child!;
},
@@ -98,27 +105,46 @@ class ProfilesFragment extends StatelessWidget {
final isMobile = state.viewMode == ViewMode.mobile;
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: !isMobile
? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
)
: EdgeInsets.zero,
child: Grid(
mainAxisSpacing: isMobile ? 8 : 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (final profile in state.profiles)
GridItem(
child: ProfileItem(
profile: profile,
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
WidgetsBinding.instance.addPostFrameCallback((_) {
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
});
return true;
},
child: ValueListenableBuilder(
valueListenable: hasPadding,
builder: (_, hasPadding, __) {
return SingleChildScrollView(
padding: !isMobile
? EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + (hasPadding ? 56 : 0),
)
: EdgeInsets.only(
bottom: 0 + (hasPadding ? 56 : 0),
),
child: Grid(
mainAxisSpacing: isMobile ? 8 : 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (final profile in state.profiles)
GridItem(
child: ProfileItem(
profile: profile,
groupValue: state.currentProfileId,
onChanged:
globalState.appController.changeProfile,
),
),
],
),
],
);
},
),
),
);

View File

@@ -199,10 +199,15 @@ class ProxiesTabView extends StatelessWidget {
_delayTest(List<Proxy> proxies) async {
for (final proxy in proxies) {
final appController = globalState.appController;
final proxyName = appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay(
Delay(name: proxy.name, value: 0),
Delay(
name: proxyName,
value: 0,
),
);
clashCore.getDelay(proxy.name).then((delay) {
clashCore.getDelay(proxyName).then((delay) {
globalState.appController.setDelay(delay);
});
}
@@ -434,7 +439,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 600,
milliseconds: 200,
),
);
_scale = Tween<double>(

View File

@@ -160,5 +160,11 @@
"checking": "Checking...",
"country": "Country",
"checkError": "Check error",
"ipCheckTimeout": "Ip check timeout"
"ipCheckTimeout": "Ip check timeout",
"search": "Search",
"allowBypass": "Allow applications to bypass VPN",
"allowBypassDesc": "Enabled to some applications can bypass VPN",
"externalController": "ExternalController",
"externalControllerDesc": "Enabled to control the clash on port 9090",
"ipv6Desc": "Enabled to will allow it to receive ipv6 traffic"
}

View File

@@ -160,5 +160,11 @@
"checking": "检测中...",
"country": "区域",
"checkError": "检测失败",
"ipCheckTimeout": "Ip检测超时"
"ipCheckTimeout": "Ip检测超时",
"search": "搜索",
"allowBypass": "允许应用绕过vpn",
"allowBypassDesc": "开启后部分应用可绕过VPN",
"externalController": "外部控制器",
"externalControllerDesc": "开启后可通过9090端口控制clash内核",
"ipv6Desc": "开启后将可以接收ipv6流量"
}

View File

@@ -40,6 +40,10 @@ class MessageLookup extends MessageLookupByLibrary {
"addressTip": MessageLookupByLibrary.simpleMessage(
"Please enter a valid WebDAV address"),
"ago": MessageLookupByLibrary.simpleMessage(" Ago"),
"allowBypass": MessageLookupByLibrary.simpleMessage(
"Allow applications to bypass VPN"),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage(
"Enabled to some applications can bypass VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("AllowLan"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage(
"Allow access proxy through the LAN"),
@@ -112,6 +116,10 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
"externalController":
MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"Enabled to control the clash on port 9090"),
"externalResources":
MessageLookupByLibrary.simpleMessage("External resources"),
"file": MessageLookupByLibrary.simpleMessage("File"),
@@ -127,6 +135,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Import from URL"),
"ipCheckTimeout":
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
"Enabled to will allow it to receive ipv6 traffic"),
"just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"light": MessageLookupByLibrary.simpleMessage("Light"),
@@ -212,6 +222,7 @@ class MessageLookup extends MessageLookupByLibrary {
"External resource related info"),
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
"selected": MessageLookupByLibrary.simpleMessage("Selected"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"),

View File

@@ -36,6 +36,9 @@ class MessageLookup extends MessageLookupByLibrary {
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"ago": MessageLookupByLibrary.simpleMessage(""),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过vpn"),
"allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
@@ -93,6 +96,9 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后可通过9090端口控制clash内核"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
@@ -103,6 +109,7 @@ class MessageLookup extends MessageLookupByLibrary {
"hours": MessageLookupByLibrary.simpleMessage("小时"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
@@ -171,6 +178,7 @@ class MessageLookup extends MessageLookupByLibrary {
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("搜索"),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"),

View File

@@ -1669,6 +1669,66 @@ class AppLocalizations {
args: [],
);
}
/// `Search`
String get search {
return Intl.message(
'Search',
name: 'search',
desc: '',
args: [],
);
}
/// `Allow applications to bypass VPN`
String get allowBypass {
return Intl.message(
'Allow applications to bypass VPN',
name: 'allowBypass',
desc: '',
args: [],
);
}
/// `Enabled to some applications can bypass VPN`
String get allowBypassDesc {
return Intl.message(
'Enabled to some applications can bypass VPN',
name: 'allowBypassDesc',
desc: '',
args: [],
);
}
/// `ExternalController`
String get externalController {
return Intl.message(
'ExternalController',
name: 'externalController',
desc: '',
args: [],
);
}
/// `Enabled to control the clash on port 9090`
String get externalControllerDesc {
return Intl.message(
'Enabled to control the clash on port 9090',
name: 'externalControllerDesc',
desc: '',
args: [],
);
}
/// `Enabled to will allow it to receive ipv6 traffic`
String get ipv6Desc {
return Intl.message(
'Enabled to will allow it to receive ipv6 traffic',
name: 'ipv6Desc',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

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

View File

@@ -107,7 +107,7 @@ class AppState with ChangeNotifier {
} else {
final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return type;
return "$type(${groups[index].now})";
return "$type(${groups[index].now ?? '*'})";
}
}

View File

@@ -16,7 +16,7 @@ class Tun with _$Tun {
const factory Tun({
@Default(false) bool enable,
@Default(appName) String device,
@Default(TunStack.mixed) TunStack stack,
@Default(TunStack.gvisor) TunStack stack,
@JsonKey(name: "dns-hijack") @Default(["any:53"]) List<String> dnsHijack,
}) = _Tun;
@@ -108,6 +108,8 @@ class Dns {
class ClashConfig extends ChangeNotifier {
int _mixedPort;
bool _allowLan;
bool _ipv6;
String _externalController;
Mode _mode;
LogLevel _logLevel;
Tun _tun;
@@ -118,15 +120,19 @@ class ClashConfig extends ChangeNotifier {
int? mixedPort,
Mode? mode,
bool? allowLan,
bool? ipv6,
LogLevel? logLevel,
String? externalController,
Tun? tun,
Dns? dns,
List<String>? rules,
}) : _mixedPort = mixedPort ?? 7890,
_mode = mode ?? Mode.rule,
_ipv6 = ipv6 ?? false,
_allowLan = allowLan ?? false,
_logLevel = logLevel ?? LogLevel.info,
_tun = tun ?? const Tun(),
_externalController = externalController ?? '',
_dns = dns ?? Dns(),
_rules = rules ?? [];
@@ -169,6 +175,26 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(name: "external-controller", defaultValue: '')
String get externalController => _externalController;
set externalController(String value) {
if (_externalController != value) {
_externalController = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false)
bool get ipv6 => _ipv6;
set ipv6(bool value) {
if (_ipv6 != value) {
_ipv6 = value;
notifyListeners();
}
}
Tun get tun => _tun;
set tun(Tun value) {

View File

@@ -8,6 +8,7 @@ import '../common/common.dart';
import 'models.dart';
part 'generated/config.g.dart';
part 'generated/config.freezed.dart';
@freezed
@@ -23,6 +24,17 @@ class AccessControl with _$AccessControl {
_$AccessControlFromJson(json);
}
@freezed
class Props with _$Props {
const factory Props({
AccessControl? accessControl,
bool? allowBypass,
}) = _Props;
factory Props.fromJson(Map<String, Object?> json) =>
_$PropsFromJson(json);
}
@JsonSerializable()
class Config extends ChangeNotifier {
List<Profile> _profiles;
@@ -41,6 +53,7 @@ class Config extends ChangeNotifier {
AccessControl _accessControl;
bool _isAnimateToPage;
bool _autoCheckUpdate;
bool _allowBypass;
DAV? _dav;
Config()
@@ -57,7 +70,8 @@ class Config extends ChangeNotifier {
_isAccessControl = false,
_autoCheckUpdate = true,
_accessControl = const AccessControl(),
_isAnimateToPage = true;
_isAnimateToPage = true,
_allowBypass = true;
deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList();
@@ -305,8 +319,22 @@ class Config extends ChangeNotifier {
}
}
update(
[Config? config, RecoveryOption recoveryOptions = RecoveryOption.all]) {
@JsonKey(defaultValue: true)
bool get allowBypass {
return _allowBypass;
}
set allowBypass(bool value) {
if (_allowBypass != value) {
_allowBypass = value;
notifyListeners();
}
}
update([
Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all,
]) {
if (config != null) {
_profiles = config._profiles;
for (final profile in config._profiles) {
@@ -325,6 +353,7 @@ class Config extends ChangeNotifier {
_openLog = config._openLog;
_themeMode = config._themeMode;
_locale = config._locale;
_allowBypass = config._allowBypass;
_primaryColor = config._primaryColor;
_proxiesSortType = config._proxiesSortType;
_isMinimizeOnExit = config._isMinimizeOnExit;

View File

@@ -135,7 +135,7 @@ class _$TunImpl implements _Tun {
const _$TunImpl(
{this.enable = false,
this.device = appName,
this.stack = TunStack.mixed,
this.stack = TunStack.gvisor,
@JsonKey(name: "dns-hijack")
final List<String> dnsHijack = const ["any:53"]})
: _dnsHijack = dnsHijack;

View File

@@ -40,6 +40,7 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig(
mode: $enumDecodeNullable(_$ModeEnumMap, json['mode']),
allowLan: json['allow-lan'] as bool?,
logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']),
externalController: json['external-controller'] as String? ?? '',
tun: json['tun'] == null
? null
: Tun.fromJson(json['tun'] as Map<String, dynamic>),
@@ -56,6 +57,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'mode': _$ModeEnumMap[instance.mode]!,
'allow-lan': instance.allowLan,
'log-level': _$LogLevelEnumMap[instance.logLevel]!,
'external-controller': instance.externalController,
'tun': instance.tun,
'dns': instance.dns,
'rules': instance.rules,
@@ -79,7 +81,7 @@ _$TunImpl _$$TunImplFromJson(Map<String, dynamic> json) => _$TunImpl(
enable: json['enable'] as bool? ?? false,
device: json['device'] as String? ?? appName,
stack: $enumDecodeNullable(_$TunStackEnumMap, json['stack']) ??
TunStack.mixed,
TunStack.gvisor,
dnsHijack: (json['dns-hijack'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??

View File

@@ -239,3 +239,172 @@ abstract class _AccessControl implements AccessControl {
_$$AccessControlImplCopyWith<_$AccessControlImpl> get copyWith =>
throw _privateConstructorUsedError;
}
Props _$PropsFromJson(Map<String, dynamic> json) {
return _Props.fromJson(json);
}
/// @nodoc
mixin _$Props {
AccessControl? get accessControl => throw _privateConstructorUsedError;
bool? get allowBypass => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$PropsCopyWith<Props> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PropsCopyWith<$Res> {
factory $PropsCopyWith(Props value, $Res Function(Props) then) =
_$PropsCopyWithImpl<$Res, Props>;
@useResult
$Res call({AccessControl? accessControl, bool? allowBypass});
$AccessControlCopyWith<$Res>? get accessControl;
}
/// @nodoc
class _$PropsCopyWithImpl<$Res, $Val extends Props>
implements $PropsCopyWith<$Res> {
_$PropsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accessControl = freezed,
Object? allowBypass = freezed,
}) {
return _then(_value.copyWith(
accessControl: freezed == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl?,
allowBypass: freezed == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
as bool?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$AccessControlCopyWith<$Res>? get accessControl {
if (_value.accessControl == null) {
return null;
}
return $AccessControlCopyWith<$Res>(_value.accessControl!, (value) {
return _then(_value.copyWith(accessControl: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$PropsImplCopyWith<$Res> implements $PropsCopyWith<$Res> {
factory _$$PropsImplCopyWith(
_$PropsImpl value, $Res Function(_$PropsImpl) then) =
__$$PropsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({AccessControl? accessControl, bool? allowBypass});
@override
$AccessControlCopyWith<$Res>? get accessControl;
}
/// @nodoc
class __$$PropsImplCopyWithImpl<$Res>
extends _$PropsCopyWithImpl<$Res, _$PropsImpl>
implements _$$PropsImplCopyWith<$Res> {
__$$PropsImplCopyWithImpl(
_$PropsImpl _value, $Res Function(_$PropsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accessControl = freezed,
Object? allowBypass = freezed,
}) {
return _then(_$PropsImpl(
accessControl: freezed == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl?,
allowBypass: freezed == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$PropsImpl implements _Props {
const _$PropsImpl({this.accessControl, this.allowBypass});
factory _$PropsImpl.fromJson(Map<String, dynamic> json) =>
_$$PropsImplFromJson(json);
@override
final AccessControl? accessControl;
@override
final bool? allowBypass;
@override
String toString() {
return 'Props(accessControl: $accessControl, allowBypass: $allowBypass)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PropsImpl &&
(identical(other.accessControl, accessControl) ||
other.accessControl == accessControl) &&
(identical(other.allowBypass, allowBypass) ||
other.allowBypass == allowBypass));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, accessControl, allowBypass);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
__$$PropsImplCopyWithImpl<_$PropsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$PropsImplToJson(
this,
);
}
}
abstract class _Props implements Props {
const factory _Props(
{final AccessControl? accessControl,
final bool? allowBypass}) = _$PropsImpl;
factory _Props.fromJson(Map<String, dynamic> json) = _$PropsImpl.fromJson;
@override
AccessControl? get accessControl;
@override
bool? get allowBypass;
@override
@JsonKey(ignore: true)
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -32,7 +32,8 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
: DAV.fromJson(json['dav'] as Map<String, dynamic>)
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true;
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -52,6 +53,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'isAnimateToPage': instance.isAnimateToPage,
'isCompatible': instance.isCompatible,
'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass,
};
const _$ThemeModeEnumMap = {
@@ -93,3 +95,17 @@ const _$AccessControlModeEnumMap = {
AccessControlMode.acceptSelected: 'acceptSelected',
AccessControlMode.rejectSelected: 'rejectSelected',
};
_$PropsImpl _$$PropsImplFromJson(Map<String, dynamic> json) => _$PropsImpl(
accessControl: json['accessControl'] == null
? null
: AccessControl.fromJson(
json['accessControl'] as Map<String, dynamic>),
allowBypass: json['allowBypass'] as bool?,
);
Map<String, dynamic> _$$PropsImplToJson(_$PropsImpl instance) =>
<String, dynamic>{
'accessControl': instance.accessControl,
'allowBypass': instance.allowBypass,
};

View File

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

View File

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

View File

@@ -61,8 +61,14 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) async {
final args =
config.isAccessControl ? json.encode(config.accessControl) : null;
final args = config.isAccessControl
? json.encode(
Props(
accessControl: config.accessControl,
allowBypass: config.allowBypass,
),
)
: null;
await proxyManager.startProxy(
port: clashConfig.mixedPort,
args: args,
@@ -105,12 +111,6 @@ class GlobalState {
clashConfig: clashConfig,
);
}
if (!appState.isInit) return;
await applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
updateCoreVersionInfo(appState);
}
@@ -133,19 +133,6 @@ class GlobalState {
});
}
updateNavigationItems({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) {
final group = appState.currentGroups;
final hasProfile = config.profiles.isNotEmpty;
appState.navigationItems = navigation.getItems(
openLogs: config.openLogs,
hasProxies: group.isNotEmpty && hasProfile,
);
}
Future<void> updateGroups(AppState appState) async {
appState.groups = await clashCore.getProxiesGroups();
}

View File

@@ -122,7 +122,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
return floatingActionButton ?? Container();
},
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
// ignore_for_file: avoid_print
void main() async {
String input = """