Compare commits

...

2 Commits

Author SHA1 Message Date
chen08209
75af47aead Change flutter version 2024-08-15 14:34:02 +08:00
chen08209
8dafe3b0ec Support profiles sort
Support windows country flags display

Optimize proxies page and profiles page columns
2024-08-15 14:18:33 +08:00
29 changed files with 483 additions and 327 deletions

View File

@@ -87,7 +87,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
flutter-version: 3.22.x
channel: 'stable'
cache: true

Binary file not shown.

View File

@@ -135,7 +135,6 @@ class ApplicationState extends State<Application> {
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,

View File

@@ -29,6 +29,7 @@ extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
? copyWith(
surface: Colors.black,
background: Colors.black,
surfaceContainer: surfaceContainer.darken(0.05),
)
: this;

View File

@@ -47,6 +47,7 @@ class DAVClient {
}
Future<List<int>> recovery() async {
await client.mkdir("$root");
final data = await client.read(backupFile);
return data;
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'package:fl_clash/common/common.dart';
@@ -191,22 +192,18 @@ class Other {
return ViewMode.desktop;
}
int getColumns(ViewMode viewMode, int currentColumns) {
final targetColumnsArray = viewModeColumnsMap[viewMode]!;
if (targetColumnsArray.contains(currentColumns)) {
return currentColumns;
}
return targetColumnsArray.first;
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
final columns = max((viewWidth / 300).ceil(), 2);
return switch (proxiesLayout) {
ProxiesLayout.tight => columns - 1,
ProxiesLayout.standard => columns,
ProxiesLayout.loose => columns + 1,
};
}
String getColumnsTextForInt(int number) {
return switch (number) {
1 => appLocalizations.oneColumn,
2 => appLocalizations.twoColumns,
3 => appLocalizations.threeColumns,
4 => appLocalizations.fourColumns,
int() => throw UnimplementedError(),
};
int getProfilesColumns(double viewWidth) {
return max((viewWidth / 400).floor(), 1);
}
String getBackupFileName() {

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
@@ -49,7 +50,9 @@ class Request {
.get(
url,
options: Options(
headers: {"User-Agent": globalState.appController.clashConfig.globalUa},
headers: {
"User-Agent": globalState.appController.clashConfig.globalUa
},
responseType: ResponseType.bytes,
),
)
@@ -84,10 +87,13 @@ class Request {
};
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) {
for (final source in _ipInfoSources.entries.toList()..shuffle(Random())) {
try {
final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
)
.timeout(
httpTimeoutDuration,
);

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:archive/archive.dart';
import 'package:fl_clash/common/archive.dart';
@@ -414,7 +415,6 @@ class AppController {
addProfileFormURL(url);
}
int get columns => other.getColumns(appState.viewMode, config.proxiesColumns);
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {

View File

@@ -86,4 +86,6 @@ enum CommonCardType { plain, filled }
enum ProxiesType { tab, list }
enum ProxiesLayout{ loose, standard, tight }
enum ProxyCardType { expand, shrink, min }

View File

@@ -1,4 +1,4 @@
import 'package:country_flags/country_flags.dart';
import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -18,6 +18,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
final timeoutNotifier = ValueNotifier<bool>(false);
bool? _preIsStart;
Function? _checkIpDebounce;
CancelToken? cancelToken;
_checkIp() async {
final appState = globalState.appController.appState;
@@ -28,13 +29,19 @@ class _NetworkDetectionState extends State<NetworkDetection> {
if (_preIsStart == false && _preIsStart == isStart) return;
_preIsStart = isStart;
ipInfoNotifier.value = null;
final ipInfo = await request.checkIp();
if (cancelToken != null) {
cancelToken!.cancel();
_preIsStart = null;
timeoutNotifier.value == false;
cancelToken = null;
}
cancelToken = CancelToken();
final ipInfo = await request.checkIp(cancelToken: cancelToken);
if (ipInfo == null) {
timeoutNotifier.value = true;
return;
} else {
timeoutNotifier.value = false;
}
timeoutNotifier.value = false;
ipInfoNotifier.value = ipInfo;
}
@@ -60,9 +67,19 @@ class _NetworkDetectionState extends State<NetworkDetection> {
timeoutNotifier.dispose();
}
String countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase();
if (code.length != 2) {
return countryCode;
}
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
}
@override
Widget build(BuildContext context) {
_checkIpDebounce = debounce(_checkIp);
_checkIpDebounce ??= debounce(_checkIp);
return _checkIpContainer(
ValueListenableBuilder<IpInfo?>(
valueListenable: ipInfoNotifier,
@@ -88,10 +105,19 @@ class _NetworkDetectionState extends State<NetworkDetection> {
flex: 1,
child: FadeBox(
child: ipInfo != null
? CountryFlag.fromCountryCode(
ipInfo.countryCode,
width: 24,
height: 24,
? Container(
alignment: Alignment.centerLeft,
height: globalState.appController.measure
.titleMediumHeight,
child: Text(
countryCodeToEmoji(ipInfo.countryCode),
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
fontFamily: "Twemoji",
),
),
)
: ValueListenableBuilder(
valueListenable: timeoutNotifier,

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/models/models.dart';
@@ -37,21 +39,11 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
}
_getColumns(ViewMode viewMode) {
switch (viewMode) {
case ViewMode.mobile:
return 1;
case ViewMode.laptop:
return 1;
case ViewMode.desktop:
return 2;
}
}
_updateProfiles() async {
final appController = globalState.appController;
final config = appController.config;
final profiles = appController.config.profiles;
final messages = [];
final updateProfiles = profiles.map<Future>(
(profile) async {
config.setProfile(
@@ -62,7 +54,8 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
if (profile.id == appController.config.currentProfile?.id) {
appController.applyProfile(isPrue: true);
}
} catch (_) {
} catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile(
profile.copyWith(
isUpdating: false,
@@ -71,13 +64,25 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
},
);
final titleMedium = context.textTheme.titleMedium;
await Future.wait(updateProfiles);
if (messages.isNotEmpty) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
children: [
for (final message in messages)
TextSpan(text: message, style: titleMedium)
],
),
);
}
}
_initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (!context.mounted) return;
if (!mounted) return;
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
@@ -87,6 +92,24 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
},
icon: const Icon(Icons.sync),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
final profiles = globalState.appController.config.profiles;
showSheet(
title: appLocalizations.profilesSort,
context: context,
builder: (_) => SizedBox(
height: 400,
child: ReorderableProfiles(profiles: profiles),
),
);
},
icon: const Icon(Icons.sort),
iconSize: 26,
),
];
},
);
@@ -116,7 +139,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
columns: other.getProfilesColumns(appState.viewWidth),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
@@ -124,7 +147,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
label: appLocalizations.nullProfileDesc,
);
}
final columns = _getColumns(state.viewMode);
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
@@ -137,7 +159,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
@@ -145,8 +167,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged:
globalState.appController.changeProfile,
onChanged: globalState.appController.changeProfile,
),
),
],
@@ -244,6 +265,7 @@ class ProfileItem extends StatelessWidget {
LinearProgressIndicator(
minHeight: 6,
value: progress,
backgroundColor: context.colorScheme.primary.toSoft(),
),
const SizedBox(
height: 8,
@@ -372,3 +394,129 @@ class ProfileItem extends StatelessWidget {
);
}
}
class ReorderableProfiles extends StatefulWidget {
final List<Profile> profiles;
const ReorderableProfiles({
super.key,
required this.profiles,
});
@override
State<ReorderableProfiles> createState() => _ReorderableProfilesState();
}
class _ReorderableProfilesState extends State<ReorderableProfiles> {
late List<Profile> profiles;
@override
void initState() {
super.initState();
profiles = List.from(widget.profiles);
}
Widget proxyDecorator(
Widget child,
int index,
Animation<double> animation,
) {
final profile = profiles[index];
return AnimatedBuilder(
animation: animation,
builder: (_, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
child: child,
);
},
child: Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 44,
left: 16,
),
title: Text(profile.label ?? profile.id),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 1,
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
padding: const EdgeInsets.all(12),
proxyDecorator: proxyDecorator,
onReorder: (int oldIndex, int newIndex) {
if (oldIndex == newIndex) return;
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final profile = profiles.removeAt(oldIndex);
profiles.insert(newIndex, profile);
});
},
itemBuilder: (_, index) {
final profile = profiles[index];
return Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 16,
left: 16,
),
title: Text(profile.label ?? profile.id),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
),
),
);
},
itemCount: profiles.length,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.config.profiles = profiles;
},
icon: const Icon(
Icons.check,
),
iconSize: 32,
padding: const EdgeInsets.all(8),
),
],
),
),
],
);
}
}

View File

@@ -69,23 +69,21 @@ class ProxyCard extends StatelessWidget {
if (type == ProxyCardType.min) {
return SizedBox(
height: measure.bodyMediumHeight * 1,
child: Text(
child: EmojiText(
proxy.name,
maxLines: 1,
style: context.textTheme.bodyMedium?.copyWith(
overflow: TextOverflow.ellipsis,
),
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium,
),
);
} else {
return SizedBox(
height: measure.bodyMediumHeight * 2,
child: Text(
child: EmojiText(
proxy.name,
maxLines: 2,
style: context.textTheme.bodyMedium?.copyWith(
overflow: TextOverflow.ellipsis,
),
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium,
),
);
}
@@ -155,14 +153,12 @@ class ProxyCard extends StatelessWidget {
proxy.name,
),
builder: (_, desc, __) {
return TooltipText(
text: Text(
desc,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context.textTheme.bodySmall?.color
?.toLight(),
),
return EmojiText(
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: context.textTheme.bodySmall?.color
?.toLight(),
),
);
},

View File

@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/other.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -60,7 +61,10 @@ double getScrollToSelectedOffset({
required List<Proxy> proxies,
}) {
final appController = globalState.appController;
final columns = appController.columns;
final columns = other.getProxiesColumns(
appController.appState.viewWidth,
appController.config.proxiesLayout,
);
final proxyCardType = appController.config.proxyCardType;
final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere(

View File

@@ -4,6 +4,7 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -237,7 +238,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
currentUnfoldSet: config.currentUnfoldSet,
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
sortNum: appState.sortNum,
);
},
@@ -450,7 +454,7 @@ class _ListHeaderState extends State<ListHeader>
if (currentGroupName.isNotEmpty) ...[
Flexible(
flex: 1,
child: Text(
child: EmojiText(
overflow: TextOverflow.ellipsis,
" · $currentGroupName",
style: context

View File

@@ -33,6 +33,14 @@ class ProxiesSettingWidget extends StatelessWidget {
};
}
String getTextForProxiesLayout(ProxiesLayout proxiesLayout) {
return switch (proxiesLayout) {
ProxiesLayout.tight => appLocalizations.tight,
ProxiesLayout.standard => appLocalizations.standard,
ProxiesLayout.loose => appLocalizations.loose,
};
}
List<Widget> _buildStyleSetting() {
return generateSection(
title: appLocalizations.style,
@@ -132,36 +140,28 @@ class ProxiesSettingWidget extends StatelessWidget {
);
}
List<Widget> _buildColumnsSetting() {
List<Widget> _buildLayoutSetting() {
return generateSection(
title: appLocalizations.columns,
title: appLocalizations.layout,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
scrollDirection: Axis.horizontal,
child: Selector2<AppState, Config, ColumnsSelectorState>(
selector: (_, appState, config) => ColumnsSelectorState(
columns: config.proxiesColumns,
viewMode: appState.viewMode,
),
builder: (_, state, __) {
child: Selector< Config, ProxiesLayout>(
selector: (_, config) => config.proxiesLayout,
builder: (_, proxiesLayout, __) {
final config = globalState.appController.config;
final targetColumnsArray = viewModeColumnsMap[state.viewMode]!;
final currentColumns = other.getColumns(
state.viewMode,
state.columns,
);
return Wrap(
spacing: 16,
children: [
for (final item in targetColumnsArray)
for (final item in ProxiesLayout.values)
SettingTextCard(
other.getColumnsTextForInt(item),
isSelected: item == currentColumns,
getTextForProxiesLayout(item),
isSelected: item == proxiesLayout,
onPressed: () {
config.proxiesColumns = item;
config.proxiesLayout = item;
},
)
],
@@ -183,10 +183,10 @@ class ProxiesSettingWidget extends StatelessWidget {
children: [
..._buildStyleSetting(),
..._buildSortSetting(),
..._buildColumnsSetting(),
..._buildLayoutSetting(),
..._buildSizeSetting(),
],
),
);
}
}
}

View File

@@ -284,7 +284,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
sortNum: appState.sortNum,
proxies: group.all,
groupType: group.type,

View File

@@ -236,5 +236,10 @@
"action": "Action",
"intelligentSelected": "Intelligent selection",
"clipboardImport": "Clipboard import",
"clipboardExport": "Export clipboard"
"clipboardExport": "Export clipboard",
"layout": "Layout",
"tight": "Tight",
"standard": "Standard",
"loose": "Loose",
"profilesSort": "Profiles sort"
}

View File

@@ -236,5 +236,10 @@
"action": "操作",
"intelligentSelected": "智能选择",
"clipboardImport": "剪贴板导入",
"clipboardExport": "导出剪贴板"
"clipboardExport": "导出剪贴板",
"layout": "布局",
"tight": "宽松",
"standard": "标准",
"loose": "紧凑",
"profilesSort": "配置排序"
}

View File

@@ -182,6 +182,7 @@ class MessageLookup extends MessageLookupByLibrary {
"keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"layout": MessageLookupByLibrary.simpleMessage("Layout"),
"light": MessageLookupByLibrary.simpleMessage("Light"),
"list": MessageLookupByLibrary.simpleMessage("List"),
"local": MessageLookupByLibrary.simpleMessage("Local"),
@@ -195,6 +196,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Disabling will hide the log entry"),
"logs": MessageLookupByLibrary.simpleMessage("Logs"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"),
"loose": MessageLookupByLibrary.simpleMessage("Loose"),
"min": MessageLookupByLibrary.simpleMessage("Min"),
"minimizeOnExit":
MessageLookupByLibrary.simpleMessage("Minimize on exit"),
@@ -266,6 +268,7 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"Please input the profile URL"),
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
"profilesSort": MessageLookupByLibrary.simpleMessage("Profiles sort"),
"project": MessageLookupByLibrary.simpleMessage("Project"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxiesSetting":
@@ -312,6 +315,7 @@ class MessageLookup extends MessageLookupByLibrary {
"size": MessageLookupByLibrary.simpleMessage("Size"),
"sort": MessageLookupByLibrary.simpleMessage("Sort"),
"source": MessageLookupByLibrary.simpleMessage("Source"),
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"style": MessageLookupByLibrary.simpleMessage("Style"),
@@ -334,6 +338,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Set dark mode,adjust the color"),
"themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"),
"threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"),
"tight": MessageLookupByLibrary.simpleMessage("Tight"),
"time": MessageLookupByLibrary.simpleMessage("Time"),
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),

View File

@@ -147,6 +147,7 @@ class MessageLookup extends MessageLookupByLibrary {
"keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"layout": MessageLookupByLibrary.simpleMessage("布局"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"),
"local": MessageLookupByLibrary.simpleMessage("本地"),
@@ -157,6 +158,7 @@ class MessageLookup extends MessageLookupByLibrary {
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"loose": MessageLookupByLibrary.simpleMessage("紧凑"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc":
@@ -214,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入配置URL"),
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
@@ -249,6 +252,7 @@ class MessageLookup extends MessageLookupByLibrary {
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
"sort": MessageLookupByLibrary.simpleMessage("排序"),
"source": MessageLookupByLibrary.simpleMessage("来源"),
"standard": MessageLookupByLibrary.simpleMessage("标准"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"style": MessageLookupByLibrary.simpleMessage("风格"),
@@ -269,6 +273,7 @@ class MessageLookup extends MessageLookupByLibrary {
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tight": MessageLookupByLibrary.simpleMessage("宽松"),
"time": MessageLookupByLibrary.simpleMessage("时间"),
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),

View File

@@ -2429,6 +2429,56 @@ class AppLocalizations {
args: [],
);
}
/// `Layout`
String get layout {
return Intl.message(
'Layout',
name: 'layout',
desc: '',
args: [],
);
}
/// `Tight`
String get tight {
return Intl.message(
'Tight',
name: 'tight',
desc: '',
args: [],
);
}
/// `Standard`
String get standard {
return Intl.message(
'Standard',
name: 'standard',
desc: '',
args: [],
);
}
/// `Loose`
String get loose {
return Intl.message(
'Loose',
name: 'loose',
desc: '',
args: [],
);
}
/// `Profiles sort`
String get profilesSort {
return Intl.message(
'Profiles sort',
name: 'profilesSort',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -28,9 +28,9 @@ class AccessControl with _$AccessControl {
extension AccessControlExt on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
@freezed
@@ -44,7 +44,8 @@ class CoreState with _$CoreState {
required bool onlyProxy,
}) = _CoreState;
factory CoreState.fromJson(Map<String, Object?> json) => _$CoreStateFromJson(json);
factory CoreState.fromJson(Map<String, Object?> json) =>
_$CoreStateFromJson(json);
}
@freezed
@@ -87,7 +88,7 @@ class Config extends ChangeNotifier {
bool _isCloseConnections;
ProxiesType _proxiesType;
ProxyCardType _proxyCardType;
int _proxiesColumns;
ProxiesLayout _proxiesLayout;
String _testUrl;
WindowProps _windowProps;
bool _onlyProxy;
@@ -116,9 +117,9 @@ class Config extends ChangeNotifier {
_proxyCardType = ProxyCardType.expand,
_windowProps = defaultWindowProps,
_proxiesType = ProxiesType.tab,
_proxiesColumns = 2,
_prueBlack = false,
_onlyProxy = false;
_onlyProxy = false,
_proxiesLayout = ProxiesLayout.standard;
deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList();
@@ -320,6 +321,16 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: ProxiesLayout.standard)
ProxiesLayout get proxiesLayout => _proxiesLayout;
set proxiesLayout(ProxiesLayout value) {
if (_proxiesLayout != value) {
_proxiesLayout = value;
notifyListeners();
}
}
@JsonKey(defaultValue: true)
bool get isMinimizeOnExit => _isMinimizeOnExit;
@@ -481,16 +492,6 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: 2)
int get proxiesColumns => _proxiesColumns;
set proxiesColumns(int value) {
if (_proxiesColumns != value) {
_proxiesColumns = value;
notifyListeners();
}
}
@JsonKey(name: "test-url", defaultValue: defaultTestUrl)
String get testUrl => _testUrl;

View File

@@ -23,6 +23,9 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..proxiesSortType =
$enumDecodeNullable(_$ProxiesSortTypeEnumMap, json['proxiesSortType']) ??
ProxiesSortType.none
..proxiesLayout =
$enumDecodeNullable(_$ProxiesLayoutEnumMap, json['proxiesLayout']) ??
ProxiesLayout.standard
..isMinimizeOnExit = json['isMinimizeOnExit'] as bool? ?? true
..isAccessControl = json['isAccessControl'] as bool? ?? false
..accessControl =
@@ -44,7 +47,6 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2
..testUrl =
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204'
..isExclude = json['isExclude'] as bool? ?? false
@@ -62,6 +64,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'locale': instance.locale,
'primaryColor': instance.primaryColor,
'proxiesSortType': _$ProxiesSortTypeEnumMap[instance.proxiesSortType]!,
'proxiesLayout': _$ProxiesLayoutEnumMap[instance.proxiesLayout]!,
'isMinimizeOnExit': instance.isMinimizeOnExit,
'isAccessControl': instance.isAccessControl,
'accessControl': instance.accessControl,
@@ -76,7 +79,6 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'isCloseConnections': instance.isCloseConnections,
'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!,
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
'proxiesColumns': instance.proxiesColumns,
'test-url': instance.testUrl,
'isExclude': instance.isExclude,
'windowProps': instance.windowProps,
@@ -94,6 +96,12 @@ const _$ProxiesSortTypeEnumMap = {
ProxiesSortType.name: 'name',
};
const _$ProxiesLayoutEnumMap = {
ProxiesLayout.loose: 'loose',
ProxiesLayout.standard: 'standard',
ProxiesLayout.tight: 'tight',
};
const _$ProxiesTypeEnumMap = {
ProxiesType.tab: 'tab',
ProxiesType.list: 'list',

View File

@@ -458,7 +458,7 @@ abstract class _NetworkDetectionSelectorState
mixin _$ProfilesSelectorState {
List<Profile> get profiles => throw _privateConstructorUsedError;
String? get currentProfileId => throw _privateConstructorUsedError;
ViewMode get viewMode => throw _privateConstructorUsedError;
int get columns => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProfilesSelectorStateCopyWith<ProfilesSelectorState> get copyWith =>
@@ -471,8 +471,7 @@ abstract class $ProfilesSelectorStateCopyWith<$Res> {
$Res Function(ProfilesSelectorState) then) =
_$ProfilesSelectorStateCopyWithImpl<$Res, ProfilesSelectorState>;
@useResult
$Res call(
{List<Profile> profiles, String? currentProfileId, ViewMode viewMode});
$Res call({List<Profile> profiles, String? currentProfileId, int columns});
}
/// @nodoc
@@ -491,7 +490,7 @@ class _$ProfilesSelectorStateCopyWithImpl<$Res,
$Res call({
Object? profiles = null,
Object? currentProfileId = freezed,
Object? viewMode = null,
Object? columns = null,
}) {
return _then(_value.copyWith(
profiles: null == profiles
@@ -502,10 +501,10 @@ class _$ProfilesSelectorStateCopyWithImpl<$Res,
? _value.currentProfileId
: currentProfileId // ignore: cast_nullable_to_non_nullable
as String?,
viewMode: null == viewMode
? _value.viewMode
: viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
@@ -519,8 +518,7 @@ abstract class _$$ProfilesSelectorStateImplCopyWith<$Res>
__$$ProfilesSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<Profile> profiles, String? currentProfileId, ViewMode viewMode});
$Res call({List<Profile> profiles, String? currentProfileId, int columns});
}
/// @nodoc
@@ -537,7 +535,7 @@ class __$$ProfilesSelectorStateImplCopyWithImpl<$Res>
$Res call({
Object? profiles = null,
Object? currentProfileId = freezed,
Object? viewMode = null,
Object? columns = null,
}) {
return _then(_$ProfilesSelectorStateImpl(
profiles: null == profiles
@@ -548,10 +546,10 @@ class __$$ProfilesSelectorStateImplCopyWithImpl<$Res>
? _value.currentProfileId
: currentProfileId // ignore: cast_nullable_to_non_nullable
as String?,
viewMode: null == viewMode
? _value.viewMode
: viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
@@ -562,7 +560,7 @@ class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState {
const _$ProfilesSelectorStateImpl(
{required final List<Profile> profiles,
required this.currentProfileId,
required this.viewMode})
required this.columns})
: _profiles = profiles;
final List<Profile> _profiles;
@@ -576,11 +574,11 @@ class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState {
@override
final String? currentProfileId;
@override
final ViewMode viewMode;
final int columns;
@override
String toString() {
return 'ProfilesSelectorState(profiles: $profiles, currentProfileId: $currentProfileId, viewMode: $viewMode)';
return 'ProfilesSelectorState(profiles: $profiles, currentProfileId: $currentProfileId, columns: $columns)';
}
@override
@@ -591,8 +589,7 @@ class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState {
const DeepCollectionEquality().equals(other._profiles, _profiles) &&
(identical(other.currentProfileId, currentProfileId) ||
other.currentProfileId == currentProfileId) &&
(identical(other.viewMode, viewMode) ||
other.viewMode == viewMode));
(identical(other.columns, columns) || other.columns == columns));
}
@override
@@ -600,7 +597,7 @@ class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState {
runtimeType,
const DeepCollectionEquality().hash(_profiles),
currentProfileId,
viewMode);
columns);
@JsonKey(ignore: true)
@override
@@ -614,14 +611,14 @@ abstract class _ProfilesSelectorState implements ProfilesSelectorState {
const factory _ProfilesSelectorState(
{required final List<Profile> profiles,
required final String? currentProfileId,
required final ViewMode viewMode}) = _$ProfilesSelectorStateImpl;
required final int columns}) = _$ProfilesSelectorStateImpl;
@override
List<Profile> get profiles;
@override
String? get currentProfileId;
@override
ViewMode get viewMode;
int get columns;
@override
@JsonKey(ignore: true)
_$$ProfilesSelectorStateImplCopyWith<_$ProfilesSelectorStateImpl>
@@ -2562,146 +2559,6 @@ abstract class _PackageListSelectorState implements PackageListSelectorState {
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ColumnsSelectorState {
int get columns => throw _privateConstructorUsedError;
ViewMode get viewMode => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ColumnsSelectorStateCopyWith<ColumnsSelectorState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ColumnsSelectorStateCopyWith<$Res> {
factory $ColumnsSelectorStateCopyWith(ColumnsSelectorState value,
$Res Function(ColumnsSelectorState) then) =
_$ColumnsSelectorStateCopyWithImpl<$Res, ColumnsSelectorState>;
@useResult
$Res call({int columns, ViewMode viewMode});
}
/// @nodoc
class _$ColumnsSelectorStateCopyWithImpl<$Res,
$Val extends ColumnsSelectorState>
implements $ColumnsSelectorStateCopyWith<$Res> {
_$ColumnsSelectorStateCopyWithImpl(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? columns = null,
Object? viewMode = null,
}) {
return _then(_value.copyWith(
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
viewMode: null == viewMode
? _value.viewMode
: viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,
) as $Val);
}
}
/// @nodoc
abstract class _$$ColumnsSelectorStateImplCopyWith<$Res>
implements $ColumnsSelectorStateCopyWith<$Res> {
factory _$$ColumnsSelectorStateImplCopyWith(_$ColumnsSelectorStateImpl value,
$Res Function(_$ColumnsSelectorStateImpl) then) =
__$$ColumnsSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int columns, ViewMode viewMode});
}
/// @nodoc
class __$$ColumnsSelectorStateImplCopyWithImpl<$Res>
extends _$ColumnsSelectorStateCopyWithImpl<$Res, _$ColumnsSelectorStateImpl>
implements _$$ColumnsSelectorStateImplCopyWith<$Res> {
__$$ColumnsSelectorStateImplCopyWithImpl(_$ColumnsSelectorStateImpl _value,
$Res Function(_$ColumnsSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? columns = null,
Object? viewMode = null,
}) {
return _then(_$ColumnsSelectorStateImpl(
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
viewMode: null == viewMode
? _value.viewMode
: viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,
));
}
}
/// @nodoc
class _$ColumnsSelectorStateImpl implements _ColumnsSelectorState {
const _$ColumnsSelectorStateImpl(
{required this.columns, required this.viewMode});
@override
final int columns;
@override
final ViewMode viewMode;
@override
String toString() {
return 'ColumnsSelectorState(columns: $columns, viewMode: $viewMode)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ColumnsSelectorStateImpl &&
(identical(other.columns, columns) || other.columns == columns) &&
(identical(other.viewMode, viewMode) ||
other.viewMode == viewMode));
}
@override
int get hashCode => Object.hash(runtimeType, columns, viewMode);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ColumnsSelectorStateImplCopyWith<_$ColumnsSelectorStateImpl>
get copyWith =>
__$$ColumnsSelectorStateImplCopyWithImpl<_$ColumnsSelectorStateImpl>(
this, _$identity);
}
abstract class _ColumnsSelectorState implements ColumnsSelectorState {
const factory _ColumnsSelectorState(
{required final int columns,
required final ViewMode viewMode}) = _$ColumnsSelectorStateImpl;
@override
int get columns;
@override
ViewMode get viewMode;
@override
@JsonKey(ignore: true)
_$$ColumnsSelectorStateImplCopyWith<_$ColumnsSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProxiesListHeaderSelectorState {
double get offset => throw _privateConstructorUsedError;

View File

@@ -37,7 +37,7 @@ class ProfilesSelectorState with _$ProfilesSelectorState {
const factory ProfilesSelectorState({
required List<Profile> profiles,
required String? currentProfileId,
required ViewMode viewMode,
required int columns,
}) = _ProfilesSelectorState;
}
@@ -172,14 +172,6 @@ extension PackageListSelectorStateExt on PackageListSelectorState {
}
}
@freezed
class ColumnsSelectorState with _$ColumnsSelectorState {
const factory ColumnsSelectorState({
required int columns,
required ViewMode viewMode,
}) = _ColumnsSelectorState;
}
@freezed
class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState {
const factory ProxiesListHeaderSelectorState({

View File

@@ -60,7 +60,6 @@ class HomePage extends StatelessWidget {
),
selectedLabelTextStyle: context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
unselectedLabelTextStyle: context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:emoji_regex/emoji_regex.dart';
import '../state.dart';
@@ -30,3 +31,63 @@ class TooltipText extends StatelessWidget {
);
}
}
class EmojiText extends StatelessWidget {
final String text;
final TextStyle? style;
final int? maxLines;
final TextOverflow? overflow;
const EmojiText(
this.text, {
super.key,
this.maxLines,
this.overflow,
this.style,
});
List<TextSpan> _buildTextSpans(String emojis) {
final List<TextSpan> spans = [];
final matches = emojiRegex().allMatches(text);
int lastMatchEnd = 0;
for (final match in matches) {
if (match.start > lastMatchEnd) {
spans.add(
TextSpan(
text: text.substring(lastMatchEnd, match.start), style: style),
);
}
spans.add(
TextSpan(
text:match.group(0),
style: style?.copyWith(
fontFamily: "Twemoji",
),
),
);
lastMatchEnd = match.end;
}
if (lastMatchEnd < text.length) {
spans.add(
TextSpan(
text: text.substring(lastMatchEnd),
style: style,
),
);
}
return spans;
}
@override
Widget build(BuildContext context) {
return RichText(
maxLines: maxLines,
overflow: overflow ?? TextOverflow.clip,
text: TextSpan(
children: _buildTextSpans(text),
),
);
}
}

View File

@@ -193,14 +193,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
country_flags:
dependency: "direct main"
description:
name: country_flags
sha256: dbc4f76e7c801619b2d841023e0327191ba00663f1f1b4770394e9bc6632c444
url: "https://pub.dev"
source: hosted
version: "2.2.0"
cross_file:
dependency: transitive
description:
@@ -249,6 +241,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.7.0"
emoji_regex:
dependency: "direct main"
description:
name: emoji_regex
sha256: "3a25dd4d16f98b6f76dc37cc9ae49b8511891ac4b87beac9443a1e9f4634b6c7"
url: "https://pub.dev"
source: hosted
version: "0.0.5"
fake_async:
dependency: transitive
description:
@@ -541,22 +541,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.3.1"
jovial_misc:
dependency: transitive
description:
name: jovial_misc
sha256: f6e64f789ee311025bb367be9c9afe9759f76dd8209070b7f38e735b5f529eb1
url: "https://pub.dev"
source: hosted
version: "0.8.5"
jovial_svg:
dependency: transitive
description:
name: jovial_svg
sha256: "000cafe183bdeba28f76d65bf93fc9b636d6f7eaa7e2a33e80c4345a28866ea8"
url: "https://pub.dev"
source: hosted
version: "1.1.21"
js:
dependency: transitive
description:
@@ -593,18 +577,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
@@ -649,10 +633,10 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.8.0"
menu_base:
dependency: transitive
description:
@@ -665,10 +649,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.12.0"
mime:
dependency: transitive
description:
@@ -797,14 +781,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool:
dependency: transitive
description:
@@ -1029,10 +1005,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.7.0"
timing:
dependency: transitive
description:
@@ -1133,10 +1109,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "14.2.4"
version: "14.2.1"
watcher:
dependency: transitive
description:
@@ -1251,4 +1227,4 @@ packages:
version: "0.2.3"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"
flutter: ">=3.22.3"

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.52+202408111
version: 0.8.53+202408151
environment:
sdk: '>=3.1.0 <4.0.0'
@@ -37,13 +37,13 @@ dependencies:
image: ^4.1.7
webdav_client: ^1.2.2
dio: ^5.4.3+1
country_flags: ^2.2.0
win32: ^5.5.1
ffi: ^2.1.2
re_editor: ^0.3.1
re_highlight: ^0.0.3
archive: ^3.6.1
lpinyin: ^2.0.3
emoji_regex: ^0.0.5
dev_dependencies:
flutter_test:
sdk: flutter
@@ -58,8 +58,13 @@ flutter:
uses-material-design: true
assets:
- assets/data/
- assets/fonts/
- assets/images/
- assets/images/avatars/
fonts:
- family: Twemoji
fonts:
- asset: assets/fonts/Twemoji.Mozilla.ttf
ffigen:
name: "ClashFFI"
output: 'lib/clash/generated/clash_ffi.dart'