Compare commits

...

6 Commits

Author SHA1 Message Date
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
33 changed files with 529 additions and 364 deletions

View File

@@ -3,7 +3,7 @@ name: build
on: on:
push: push:
tags: tags:
- '*' - 'v*'
jobs: jobs:
build: build:
@@ -82,7 +82,7 @@ jobs:
upload-release: upload-release:
if: ${{ !endsWith(github.ref, '-debug') }} if: ${{ !contains(github.ref, '+') }}
permissions: write-all permissions: write-all
needs: [ build ] needs: [ build ]
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.net.ProxyInfo import android.net.ProxyInfo
@@ -13,15 +12,13 @@ import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat 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.GlobalState
import com.follow.clash.MainActivity import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.models.AccessControl import com.follow.clash.models.AccessControl
import com.follow.clash.models.AccessControlMode import com.follow.clash.models.AccessControlMode
@SuppressLint("WrongConstant")
class FlClashVpnService : VpnService() { class FlClashVpnService : VpnService() {
@@ -100,10 +97,10 @@ class FlClashVpnService : VpnService() {
} }
private val notificationBuilder by lazy { private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java) 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( PendingIntent.getActivity(
this, this,
0, 0,
@@ -119,43 +116,43 @@ class FlClashVpnService : VpnService() {
) )
} }
val icon = IconCompat.createWithResource(this, this.applicationInfo.icon)
with(NotificationCompat.Builder(this, CHANNEL)) { with(NotificationCompat.Builder(this, CHANNEL)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { setSmallIcon(R.drawable.ic_stat_name)
setSmallIcon(icon)
}
setContentTitle("FlClash") setContentTitle("FlClash")
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
setContentIntent(pendingIntent) setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE) 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) setOngoing(true)
setShowWhen(false) setShowWhen(false)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
setAutoCancel(true);
} }
} }
@SuppressLint("ForegroundServiceType", "WrongConstant")
fun startForeground(title: String, content: String) { fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = val channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
channel.setShowBadge(false)
} }
val notification = val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build() 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) startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else { }else{
startForeground(notificationId, notification) startForeground(notificationId, notification)
} }
} }
private fun stopForeground() { private fun stopForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 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

@@ -336,8 +336,8 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Mode = patchConfig.Mode targetConfig.Mode = patchConfig.Mode
targetConfig.Tun.Enable = patchConfig.Tun.Enable targetConfig.Tun.Enable = patchConfig.Tun.Enable
targetConfig.Tun.Device = patchConfig.Tun.Device targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack //targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack //targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.GeodataLoader = "standard" targetConfig.GeodataLoader = "standard"
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
if targetConfig.DNS.Enable == false { if targetConfig.DNS.Enable == false {

View File

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

View File

@@ -8,6 +8,13 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.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 { class AccessFragment extends StatefulWidget {
const AccessFragment({super.key}); const AccessFragment({super.key});
@@ -83,137 +90,64 @@ class _AccessFragmentState extends State<AccessFragment> {
); );
} }
Widget _buildSelectedAllButton({ Widget _buildSearchButton(List<Package> packages) {
required bool isSelectedAll, return IconButton(
required List<String> allValueList, tooltip: appLocalizations.search,
}) { onPressed: () {
return Builder( showSearch(
builder: (context) { context: context,
final tooltip = isSelectedAll delegate: AccessControlSearchDelegate(
? appLocalizations.cancelSelectAll packages: packages,
: appLocalizations.selectAll; ),
return IconButton( ).then((_) => {setState(() {})});
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),
);
}, },
icon: const Icon(Icons.search),
); );
} }
Widget _actionHeader({ // Widget _buildSelectedAllButton({
required bool isAccessControl, // required bool isSelectedAll,
required List<String> valueList, // required List<String> allValueList,
required String describe, // }) {
required List<String> packageNameList, // return Builder(
}) { // builder: (context) {
return AbsorbPointer( // final tooltip = isSelectedAll
absorbing: !isAccessControl, // ? appLocalizations.cancelSelectAll
child: Padding( // : appLocalizations.selectAll;
padding: const EdgeInsets.only( // return IconButton(
top: 4, // tooltip: tooltip,
bottom: 4, // onPressed: () {
left: 16, // final config = globalState.appController.config;
right: 8, // final isAccept =
), // config.accessControl.mode == AccessControlMode.acceptSelected;
child: Row( //
mainAxisAlignment: MainAxisAlignment.spaceBetween, // if (isSelectedAll) {
mainAxisSize: MainAxisSize.max, // config.accessControl = switch (isAccept) {
children: [ // true => config.accessControl.copyWith(
Expanded( // acceptList: [],
child: IntrinsicHeight( // ),
child: Column( // false => config.accessControl.copyWith(
mainAxisSize: MainAxisSize.max, // rejectList: [],
crossAxisAlignment: CrossAxisAlignment.start, // ),
children: [ // };
Expanded( // } else {
child: Row( // config.accessControl = switch (isAccept) {
children: [ // true => config.accessControl.copyWith(
Flexible( // acceptList: allValueList,
child: Text( // ),
appLocalizations.selected, // false => config.accessControl.copyWith(
style: Theme.of(context) // rejectList: allValueList,
.textTheme // ),
.labelLarge // };
?.copyWith( // }
color: // },
Theme.of(context).colorScheme.primary, // icon: isSelectedAll
), // ? const Icon(Icons.deselect)
), // : const Icon(Icons.select_all),
), // );
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: _buildSelectedAllButton(
isSelectedAll: const ListEquality<String>()
.equals(valueList, packageNameList),
allValueList: packageNameList,
),
),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
),
),
);
}
Widget _buildPackageList() { Widget _buildPackageList() {
return ValueListenableBuilder( return ValueListenableBuilder(
@@ -252,14 +186,11 @@ class _AccessFragmentState extends State<AccessFragment> {
accessControlMode == AccessControlMode.acceptSelected accessControlMode == AccessControlMode.acceptSelected
? acceptPackages ? acceptPackages
: rejectPackages; : rejectPackages;
final currentList = final currentList = accessControl.currentList;
accessControlMode == AccessControlMode.acceptSelected
? accessControl.acceptList
: accessControl.rejectList;
final currentPackages = isFilterSystemApp final currentPackages = isFilterSystemApp
? packages ? packages
.where((element) => element.isSystem == false) .where((element) => element.isSystem == false)
.toList() .toList()
: packages; : packages;
final packageNameList = final packageNameList =
currentPackages.map((e) => e.packageName).toList(); currentPackages.map((e) => e.packageName).toList();
@@ -272,11 +203,82 @@ class _AccessFragmentState extends State<AccessFragment> {
status: !isAccessControl, status: !isAccessControl,
child: Column( child: Column(
children: [ children: [
_actionHeader( AbsorbPointer(
isAccessControl: isAccessControl, absorbing: !isAccessControl,
valueList: valueList, child: Padding(
describe: describe, padding: const EdgeInsets.only(
packageNameList: packageNameList, 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( Expanded(
flex: 1, flex: 1,
@@ -429,3 +431,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 = 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) {
final packages = _results;
return _packageList(packages);
}
@override
Widget buildSuggestions(BuildContext context) {
return _packageList(packages);
}
}

View File

@@ -17,9 +17,16 @@ enum ProfileActions {
delete, delete,
} }
class ProfilesFragment extends StatelessWidget { class ProfilesFragment extends StatefulWidget {
const ProfilesFragment({super.key}); const ProfilesFragment({super.key});
@override
State<ProfilesFragment> createState() => _ProfilesFragmentState();
}
class _ProfilesFragmentState extends State<ProfilesFragment> {
final hasPadding = ValueNotifier<bool>(false);
_handleShowAddExtendPage() { _handleShowAddExtendPage() {
showExtendPage( showExtendPage(
globalState.navigatorKey.currentState!.context, globalState.navigatorKey.currentState!.context,
@@ -41,7 +48,7 @@ class ProfilesFragment extends StatelessWidget {
} }
} }
_initScaffoldState(BuildContext context) { _initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) { (_) {
final commonScaffoldState = final commonScaffoldState =
@@ -78,7 +85,7 @@ class ProfilesFragment extends StatelessWidget {
selector: (_, appState) => appState.currentLabel == 'profiles', selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) { builder: (_, isCurrent, child) {
if (isCurrent) { if (isCurrent) {
_initScaffoldState(context); _initScaffoldState();
} }
return child!; return child!;
}, },
@@ -98,27 +105,46 @@ class ProfilesFragment extends StatelessWidget {
final isMobile = state.viewMode == ViewMode.mobile; final isMobile = state.viewMode == ViewMode.mobile;
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: SingleChildScrollView( child: NotificationListener<ScrollNotification>(
padding: !isMobile onNotification: (scrollNotification) {
? const EdgeInsets.symmetric( WidgetsBinding.instance.addPostFrameCallback((_) {
horizontal: 16, hasPadding.value =
vertical: 16, scrollNotification.metrics.maxScrollExtent > 0;
) });
: EdgeInsets.zero, return true;
child: Grid( },
mainAxisSpacing: isMobile ? 8 : 16, child: ValueListenableBuilder(
crossAxisSpacing: 16, valueListenable: hasPadding,
crossAxisCount: columns, builder: (_, hasPadding, __) {
children: [ return SingleChildScrollView(
for (final profile in state.profiles) padding: !isMobile
GridItem( ? EdgeInsets.only(
child: ProfileItem( left: 16,
profile: profile, right: 16,
groupValue: state.currentProfileId, top: 16,
onChanged: globalState.appController.changeProfile, 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 { _delayTest(List<Proxy> proxies) async {
for (final proxy in proxies) { for (final proxy in proxies) {
final appController = globalState.appController;
final proxyName = appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay( 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); globalState.appController.setDelay(delay);
}); });
} }
@@ -434,7 +439,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
duration: const Duration( duration: const Duration(
milliseconds: 600, milliseconds: 200,
), ),
); );
_scale = Tween<double>( _scale = Tween<double>(

View File

@@ -160,5 +160,6 @@
"checking": "Checking...", "checking": "Checking...",
"country": "Country", "country": "Country",
"checkError": "Check error", "checkError": "Check error",
"ipCheckTimeout": "Ip check timeout" "ipCheckTimeout": "Ip check timeout",
"search": "Search"
} }

View File

@@ -160,5 +160,6 @@
"checking": "检测中...", "checking": "检测中...",
"country": "区域", "country": "区域",
"checkError": "检测失败", "checkError": "检测失败",
"ipCheckTimeout": "Ip检测超时" "ipCheckTimeout": "Ip检测超时",
"search": "搜索"
} }

View File

@@ -212,6 +212,7 @@ class MessageLookup extends MessageLookupByLibrary {
"External resource related info"), "External resource related info"),
"rule": MessageLookupByLibrary.simpleMessage("Rule"), "rule": MessageLookupByLibrary.simpleMessage("Rule"),
"save": MessageLookupByLibrary.simpleMessage("Save"), "save": MessageLookupByLibrary.simpleMessage("Save"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"), "selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
"selected": MessageLookupByLibrary.simpleMessage("Selected"), "selected": MessageLookupByLibrary.simpleMessage("Selected"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"), "settings": MessageLookupByLibrary.simpleMessage("Settings"),

View File

@@ -171,6 +171,7 @@ class MessageLookup extends MessageLookupByLibrary {
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"), "resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"rule": MessageLookupByLibrary.simpleMessage("规则"), "rule": MessageLookupByLibrary.simpleMessage("规则"),
"save": MessageLookupByLibrary.simpleMessage("保存"), "save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("搜索"),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"), "selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"), "settings": MessageLookupByLibrary.simpleMessage("设置"),

View File

@@ -1669,6 +1669,16 @@ class AppLocalizations {
args: [], args: [],
); );
} }
/// `Search`
String get search {
return Intl.message(
'Search',
name: 'search',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -107,7 +107,7 @@ class AppState with ChangeNotifier {
} else { } else {
final index = groups.indexWhere((element) => element.name == proxyName); final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return type; 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({ const factory Tun({
@Default(false) bool enable, @Default(false) bool enable,
@Default(appName) String device, @Default(appName) String device,
@Default(TunStack.mixed) TunStack stack, @Default(TunStack.gvisor) TunStack stack,
@JsonKey(name: "dns-hijack") @Default(["any:53"]) List<String> dnsHijack, @JsonKey(name: "dns-hijack") @Default(["any:53"]) List<String> dnsHijack,
}) = _Tun; }) = _Tun;

View File

@@ -8,6 +8,7 @@ import '../common/common.dart';
import 'models.dart'; import 'models.dart';
part 'generated/config.g.dart'; part 'generated/config.g.dart';
part 'generated/config.freezed.dart'; part 'generated/config.freezed.dart';
@freezed @freezed

View File

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

View File

@@ -79,7 +79,7 @@ _$TunImpl _$$TunImplFromJson(Map<String, dynamic> json) => _$TunImpl(
enable: json['enable'] as bool? ?? false, enable: json['enable'] as bool? ?? false,
device: json['device'] as String? ?? appName, device: json['device'] as String? ?? appName,
stack: $enumDecodeNullable(_$TunStackEnumMap, json['stack']) ?? stack: $enumDecodeNullable(_$TunStackEnumMap, json['stack']) ??
TunStack.mixed, TunStack.gvisor,
dnsHijack: (json['dns-hijack'] as List<dynamic>?) dnsHijack: (json['dns-hijack'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList() ?? .toList() ??

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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