From 0389b6eb293c9132c8962f76c2f198dd0e166744 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sun, 23 Jun 2024 00:26:24 +0800 Subject: [PATCH] Optimize proxy Optimize delayed sorting performance Add expansion panel proxies page Support to adjust the proxy card size Support to adjust proxies columns number --- core/platform/limit.go | 42 + core/tun.go | 7 + lib/application.dart | 1 - lib/common/constant.dart | 1 + lib/controller.dart | 46 + lib/enum/enum.dart | 11 +- lib/fragments/config.dart | 2 +- lib/fragments/dashboard/core_info.dart | 1 + lib/fragments/dashboard/intranet_ip.dart | 3 + .../dashboard/network_detection.dart | 9 +- lib/fragments/dashboard/network_speed.dart | 1 + lib/fragments/dashboard/outbound_mode.dart | 1 + lib/fragments/dashboard/traffic_usage.dart | 1 + lib/fragments/fragments.dart | 2 +- lib/fragments/profiles/profiles.dart | 2 +- lib/fragments/proxies.dart | 797 ++++++++++++++++++ lib/fragments/proxies/expansion_panel.dart | 59 -- lib/fragments/proxies/proxies.dart | 72 -- lib/fragments/proxies/tabview.dart | 470 ----------- lib/l10n/arb/intl_en.arb | 6 +- lib/l10n/arb/intl_zh_CN.arb | 8 +- lib/l10n/intl/messages_en.dart | 6 +- lib/l10n/intl/messages_zh_CN.dart | 8 +- lib/l10n/l10n.dart | 12 +- lib/models/config.dart | 53 +- lib/models/generated/config.g.dart | 22 +- lib/models/generated/profile.freezed.dart | 46 +- lib/models/generated/profile.g.dart | 5 + lib/models/generated/selector.freezed.dart | 192 +++-- lib/models/profile.dart | 1 + lib/models/selector.dart | 11 +- lib/state.dart | 13 + lib/widgets/card.dart | 45 +- lib/widgets/chip.dart | 13 +- pubspec.yaml | 2 +- 35 files changed, 1214 insertions(+), 757 deletions(-) create mode 100644 core/platform/limit.go create mode 100644 lib/fragments/proxies.dart delete mode 100644 lib/fragments/proxies/expansion_panel.dart delete mode 100644 lib/fragments/proxies/proxies.dart delete mode 100644 lib/fragments/proxies/tabview.dart diff --git a/core/platform/limit.go b/core/platform/limit.go new file mode 100644 index 0000000..0f3e24a --- /dev/null +++ b/core/platform/limit.go @@ -0,0 +1,42 @@ +//go:build android + +package platform + +import "syscall" + +var nullFd int +var maxFdCount int + +func init() { + fd, err := syscall.Open("/dev/null", syscall.O_WRONLY, 0644) + if err != nil { + panic(err.Error()) + } + + nullFd = fd + + var limit syscall.Rlimit + + if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil { + maxFdCount = 1024 + } else { + maxFdCount = int(limit.Cur) + } + + maxFdCount = maxFdCount / 4 * 3 +} + +func ShouldBlockConnection() bool { + fd, err := syscall.Dup(nullFd) + if err != nil { + return true + } + + _ = syscall.Close(fd) + + if fd > maxFdCount { + return true + } + + return false +} diff --git a/core/tun.go b/core/tun.go index 20d971b..dbe3452 100644 --- a/core/tun.go +++ b/core/tun.go @@ -4,7 +4,9 @@ package main import "C" import ( + "core/platform" t "core/tun" + "errors" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/log" "golang.org/x/sync/semaphore" @@ -59,8 +61,13 @@ func stopTun() { }() } +var errBlocked = errors.New("blocked") + func init() { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { + if platform.ShouldBlockConnection() { + return errBlocked + } return conn.Control(func(fd uintptr) { if tun != nil { tun.MarkSocket(int(fd)) diff --git a/lib/application.dart b/lib/application.dart index 807c429..3454117 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -130,7 +130,6 @@ class ApplicationState extends State { httpTimeoutDuration, (timer) async { await globalState.appController.updateGroups(); - globalState.appController.appState.sortNum++; }, ); } diff --git a/lib/common/constant.dart b/lib/common/constant.dart index 7418dc1..2f953a4 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -7,6 +7,7 @@ const coreName = "clash.meta"; const packageName = "FlClash"; const httpTimeoutDuration = Duration(milliseconds: 5000); const moreDuration = Duration(milliseconds: 100); +const animateDuration = Duration(milliseconds: 100); const defaultUpdateDuration = Duration(days: 1); const mmdbFileName = "geoip.metadb"; const geoSiteFileName = "GeoSite.dat"; diff --git a/lib/controller.dart b/lib/controller.dart index 033ec82..05e055a 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -405,9 +405,55 @@ class AppController { addProfileFormURL(url); } + int get columns => + globalState.getColumns(appState.viewMode, config.proxiesColumns); + + changeColumns() { + config.proxiesColumns = globalState.getColumns( + appState.viewMode, + columns - 1, + ); + } + updateViewWidth(double width) { WidgetsBinding.instance.addPostFrameCallback((_) { appState.viewWidth = width; }); } + + + List _sortOfName(List proxies) { + return List.of(proxies) + ..sort( + (a, b) => other.sortByChar(a.name, b.name), + ); + } + + List _sortOfDelay(List proxies) { + return proxies = List.of(proxies) + ..sort( + (a, b) { + final aDelay = appState.getDelay(a.name); + final bDelay = appState.getDelay(b.name); + if (aDelay == null && bDelay == null) { + return 0; + } + if (aDelay == null || aDelay == -1) { + return 1; + } + if (bDelay == null || bDelay == -1) { + return -1; + } + return aDelay.compareTo(bDelay); + }, + ); + } + + List getSortProxies(List proxies){ + return switch(config.proxiesSortType){ + ProxiesSortType.none => proxies, + ProxiesSortType.delay => _sortOfDelay(proxies), + ProxiesSortType.name =>_sortOfName(proxies), + }; + } } diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index ba984ea..06b06e8 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -65,7 +65,10 @@ enum RecoveryOption { onlyProfiles, } -enum ChipType { - action, - delete, -} +enum ChipType { action, delete } + +enum CommonCardType { plain, filled } + +enum ProxiesType { tab, expansion } + +enum ProxyCardType { expand, shrink } diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart index d7f0660..603f65a 100644 --- a/lib/fragments/config.dart +++ b/lib/fragments/config.dart @@ -191,7 +191,7 @@ class _ConfigFragmentState extends State { builder: (_, ipv6, __) { return ListItem.switchItem( leading: const Icon(Icons.water_outlined), - title: const Text("Ipv6"), + title: const Text("IPv6"), subtitle: Text(appLocalizations.ipv6Desc), delegate: SwitchDelegate( value: ipv6, diff --git a/lib/fragments/dashboard/core_info.dart b/lib/fragments/dashboard/core_info.dart index 5f48e9d..a080556 100644 --- a/lib/fragments/dashboard/core_info.dart +++ b/lib/fragments/dashboard/core_info.dart @@ -13,6 +13,7 @@ class CoreInfo extends StatelessWidget { selector: (_, appState) => appState.versionInfo, builder: (_, versionInfo, __) { return CommonCard( + onPressed: () {}, info: Info( label: appLocalizations.coreInfo, iconData: Icons.memory, diff --git a/lib/fragments/dashboard/intranet_ip.dart b/lib/fragments/dashboard/intranet_ip.dart index 23efedd..be597c6 100644 --- a/lib/fragments/dashboard/intranet_ip.dart +++ b/lib/fragments/dashboard/intranet_ip.dart @@ -48,6 +48,9 @@ class _IntranetIpState extends State { label: appLocalizations.intranetIp, iconData: Icons.devices, ), + onPressed: (){ + + }, child: Container( padding: const EdgeInsets.all(16).copyWith(top: 0), height: globalState.appController.measure.titleLargeHeight + 24 - 1, diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart index 6a79013..03d2203 100644 --- a/lib/fragments/dashboard/network_detection.dart +++ b/lib/fragments/dashboard/network_detection.dart @@ -78,6 +78,7 @@ class _NetworkDetectionState extends State { valueListenable: ipInfoNotifier, builder: (_, ipInfo, __) { return CommonCard( + onPressed: () {}, child: Column( children: [ Flexible( @@ -134,8 +135,9 @@ class _NetworkDetectionState extends State { ), ), Container( - height: - globalState.appController.measure.titleLargeHeight + 24 - 1, + height: globalState.appController.measure.titleLargeHeight + + 24 - + 1, alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16).copyWith(top: 0), child: FadeBox( @@ -166,7 +168,8 @@ class _NetworkDetectionState extends State { "timeout", style: context.textTheme.titleLarge ?.copyWith(color: Colors.red) - .toSoftBold.toMinus, + .toSoftBold + .toMinus, maxLines: 1, overflow: TextOverflow.ellipsis, ); diff --git a/lib/fragments/dashboard/network_speed.dart b/lib/fragments/dashboard/network_speed.dart index 2e6280f..28f60cb 100644 --- a/lib/fragments/dashboard/network_speed.dart +++ b/lib/fragments/dashboard/network_speed.dart @@ -111,6 +111,7 @@ class _NetworkSpeedState extends State { @override Widget build(BuildContext context) { return CommonCard( + onPressed: () {}, info: Info( label: appLocalizations.networkSpeed, iconData: Icons.speed, diff --git a/lib/fragments/dashboard/outbound_mode.dart b/lib/fragments/dashboard/outbound_mode.dart index 125fc46..0adcd74 100644 --- a/lib/fragments/dashboard/outbound_mode.dart +++ b/lib/fragments/dashboard/outbound_mode.dart @@ -33,6 +33,7 @@ class OutboundMode extends StatelessWidget { selector: (_, clashConfig) => clashConfig.mode, builder: (_, mode, __) { return CommonCard( + onPressed: () {}, info: Info( label: appLocalizations.outboundMode, iconData: Icons.call_split, diff --git a/lib/fragments/dashboard/traffic_usage.dart b/lib/fragments/dashboard/traffic_usage.dart index 7167013..dee6773 100644 --- a/lib/fragments/dashboard/traffic_usage.dart +++ b/lib/fragments/dashboard/traffic_usage.dart @@ -51,6 +51,7 @@ class TrafficUsage extends StatelessWidget { @override Widget build(BuildContext context) { return CommonCard( + onPressed: () {}, info: Info( label: appLocalizations.trafficUsage, iconData: Icons.data_saver_off, diff --git a/lib/fragments/fragments.dart b/lib/fragments/fragments.dart index 37f213a..a91b54e 100644 --- a/lib/fragments/fragments.dart +++ b/lib/fragments/fragments.dart @@ -1,4 +1,4 @@ -export 'proxies/proxies.dart'; +export 'proxies.dart'; export 'dashboard/dashboard.dart'; export 'tools.dart'; export 'profiles/profiles.dart'; diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index f7d4871..e495a00 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -444,7 +444,7 @@ class _ProfileItemState extends State { ), CommonPopupMenuItem( action: ProfileActions.view, - label: "查看", + label: appLocalizations.view, iconData: Icons.visibility, ), ], diff --git a/lib/fragments/proxies.dart b/lib/fragments/proxies.dart new file mode 100644 index 0000000..d2b3134 --- /dev/null +++ b/lib/fragments/proxies.dart @@ -0,0 +1,797 @@ +import 'package:collection/collection.dart'; +import 'package:fl_clash/clash/core.dart'; +import 'package:fl_clash/common/common.dart'; +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/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ProxiesFragment extends StatefulWidget { + const ProxiesFragment({super.key}); + + @override + State createState() => _ProxiesFragmentState(); +} + +class _ProxiesFragmentState extends State { + _initActions() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final commonScaffoldState = + context.findAncestorStateOfType(); + final items = [ + CommonPopupMenuItem( + action: ProxiesSortType.none, + label: appLocalizations.defaultSort, + iconData: Icons.reorder, + ), + CommonPopupMenuItem( + action: ProxiesSortType.delay, + label: appLocalizations.delaySort, + iconData: Icons.network_ping, + ), + CommonPopupMenuItem( + action: ProxiesSortType.name, + label: appLocalizations.nameSort, + iconData: Icons.sort_by_alpha, + ), + ]; + commonScaffoldState?.actions = [ + Selector( + selector: (_, config) => config.proxiesType, + builder: (_, proxiesType, __) { + return IconButton( + icon: Icon( + switch (proxiesType) { + ProxiesType.tab => Icons.view_list, + ProxiesType.expansion => Icons.view_carousel, + }, + ), + onPressed: () { + final config = globalState.appController.config; + config.proxiesType = config.proxiesType == ProxiesType.tab + ? ProxiesType.expansion + : ProxiesType.tab; + }, + ); + }, + ), + IconButton( + icon: const Icon( + Icons.view_column, + ), + onPressed: () { + globalState.appController.changeColumns(); + }, + ), + IconButton( + icon: const Icon(Icons.transform_sharp), + onPressed: () { + final config = globalState.appController.config; + config.proxyCardType = config.proxyCardType == ProxyCardType.expand + ? ProxyCardType.shrink + : ProxyCardType.expand; + }, + ), + Selector( + selector: (_, config) => config.proxiesSortType, + builder: (_, proxiesSortType, __) { + return CommonPopupMenu.radio( + items: items, + icon: const Icon(Icons.sort_sharp), + onSelected: (value) { + final config = context.read(); + config.proxiesSortType = value; + }, + selectedValue: proxiesSortType, + ); + }, + ), + const SizedBox( + width: 8, + ) + ]; + }); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => appState.currentLabel == 'proxies', + builder: (_, isCurrent, child) { + if (isCurrent) { + _initActions(); + } + return child!; + }, + child: Selector( + selector: (_, config) => config.proxiesType, + builder: (_, proxiesType, __) { + return switch (proxiesType) { + ProxiesType.tab => const ProxiesTabFragment(), + ProxiesType.expansion => const ProxiesExpansionPanelFragment(), + }; + }, + ), + ); + } +} + +class ProxiesTabFragment extends StatefulWidget { + const ProxiesTabFragment({super.key}); + + @override + State createState() => _ProxiesTabFragmentState(); +} + +class _ProxiesTabFragmentState extends State + with TickerProviderStateMixin { + TabController? _tabController; + + _handleTabControllerChange() { + final indexIsChanging = _tabController?.indexIsChanging ?? false; + if (indexIsChanging) return; + final index = _tabController?.index; + if (index == null) return; + final appController = globalState.appController; + final currentGroups = appController.appState.currentGroups; + if (currentGroups.length > index) { + appController.config.updateCurrentGroupName(currentGroups[index].name); + } + } + + @override + void dispose() { + super.dispose(); + _tabController?.dispose(); + } + + @override + Widget build(BuildContext context) { + return Selector2( + selector: (_, appState, config) { + final currentGroups = appState.currentGroups; + final groupNames = currentGroups.map((e) => e.name).toList(); + return ProxiesSelectorState( + groupNames: groupNames, + currentGroupName: config.currentGroupName, + ); + }, + shouldRebuild: (prev, next) { + if (!const ListEquality() + .equals(prev.groupNames, next.groupNames)) { + _tabController?.removeListener(_handleTabControllerChange); + _tabController?.dispose(); + _tabController = null; + return true; + } + return false; + }, + builder: (_, state, __) { + final index = state.groupNames.indexWhere( + (item) => item == state.currentGroupName, + ); + _tabController ??= TabController( + length: state.groupNames.length, + initialIndex: index == -1 ? 0 : index, + vsync: this, + )..addListener(_handleTabControllerChange); + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + controller: _tabController, + padding: const EdgeInsets.symmetric(horizontal: 16), + dividerColor: Colors.transparent, + isScrollable: true, + tabAlignment: TabAlignment.start, + overlayColor: const WidgetStatePropertyAll(Colors.transparent), + tabs: [ + for (final groupName in state.groupNames) + Tab( + text: groupName, + ), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + for (final groupName in state.groupNames) + KeepContainer( + key: ObjectKey(groupName), + child: ProxyGroupView( + groupName: groupName, + type: ProxiesType.tab, + ), + ), + ], + ), + ) + ], + ); + }, + ); + } +} + +class ProxiesExpansionPanelFragment extends StatefulWidget { + const ProxiesExpansionPanelFragment({super.key}); + + @override + State createState() => + _ProxiesExpansionPanelFragmentState(); +} + +class _ProxiesExpansionPanelFragmentState + extends State { + @override + Widget build(BuildContext context) { + return Selector2( + selector: (_, appState, config) { + final currentGroups = appState.currentGroups; + final groupNames = currentGroups.map((e) => e.name).toList(); + return ProxiesSelectorState( + groupNames: groupNames, + currentGroupName: config.currentGroupName, + ); + }, + builder: (_, state, __) { + return ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: state.groupNames.length, + itemBuilder: (_, index) { + final groupName = state.groupNames[index]; + return ProxyGroupView( + key: Key(groupName), + groupName: groupName, + type: ProxiesType.expansion, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox( + height: 16, + ); + }, + ); + }, + ); + } +} + +class ProxyGroupView extends StatefulWidget { + final String groupName; + final ProxiesType type; + + const ProxyGroupView({ + super.key, + required this.groupName, + required this.type, + }); + + @override + State createState() => _ProxyGroupViewState(); +} + +class _ProxyGroupViewState extends State { + var isLock = false; + + String get groupName => widget.groupName; + + ProxiesType get type => widget.type; + + double _getItemHeight(ProxyCardType proxyCardType) { + final isExpand = proxyCardType == ProxyCardType.expand; + final measure = globalState.appController.measure; + final baseHeight = + 12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8; + return isExpand ? baseHeight + measure.labelSmallHeight + 8 : baseHeight; + } + + _delayTest(List proxies) async { + if (isLock) return; + isLock = true; + final appController = globalState.appController; + for (final proxy in proxies) { + final proxyName = + appController.appState.getRealProxyName(proxy.name) ?? proxy.name; + globalState.appController.setDelay( + Delay( + name: proxyName, + value: 0, + ), + ); + clashCore.getDelay(proxyName).then((delay) { + globalState.appController.setDelay(delay); + }); + } + await Future.delayed(httpTimeoutDuration + moreDuration); + appController.appState.sortNum++; + isLock = false; + } + + Widget _currentProxyNameBuilder({ + required Widget Function(String) builder, + }) { + return Selector2( + selector: (_, appState, config) { + final group = appState.getGroupWithName(groupName)!; + return config.currentSelectedMap[groupName] ?? group.now ?? ''; + }, + builder: (_, value, ___) { + return builder(value); + }, + ); + } + + Widget _buildTabGroupView({ + required List proxies, + required int columns, + required ProxyCardType proxyCardType, + }) { + final sortedProxies = globalState.appController.getSortProxies( + proxies, + ); + return DelayTestButtonContainer( + onClick: () async { + await _delayTest( + proxies, + ); + }, + child: Align( + alignment: Alignment.topCenter, + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + mainAxisExtent: _getItemHeight(proxyCardType), + ), + itemCount: sortedProxies.length, + itemBuilder: (_, index) { + final proxy = sortedProxies[index]; + return _currentProxyNameBuilder(builder: (value) { + return ProxyCard( + type: proxyCardType, + key: ValueKey('$groupName.${proxy.name}'), + isSelected: value == proxy.name, + proxy: proxy, + groupName: groupName, + ); + }); + }, + ), + ), + ); + } + + Widget _buildExpansionGroupView({ + required List proxies, + required int columns, + required ProxyCardType proxyCardType, + }) { + final sortedProxies = globalState.appController.getSortProxies( + proxies, + ); + return Selector>( + selector: (_, config) => config.currentUnfoldSet, + builder: (_, currentUnfoldSet, __) { + return CommonCard( + child: ExpansionTile( + initiallyExpanded: currentUnfoldSet.contains(groupName), + iconColor: context.colorScheme.onSurfaceVariant, + onExpansionChanged: (value) { + final tempUnfoldSet = Set.from(currentUnfoldSet); + if (value) { + tempUnfoldSet.add(groupName); + } else { + tempUnfoldSet.remove(groupName); + } + globalState.appController.config.updateCurrentUnfoldSet( + tempUnfoldSet, + ); + }, + controlAffinity: ListTileControlAffinity.trailing, + title: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 1, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(groupName), + const SizedBox( + height: 4, + ), + Flexible( + flex: 1, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + groupName, + style: context.textTheme.labelMedium?.toLight, + ), + Flexible( + flex: 1, + child: _currentProxyNameBuilder( + builder: (value) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + if (value.isNotEmpty) ...[ + Icon( + Icons.arrow_right, + color: context + .colorScheme.onSurfaceVariant, + ), + Flexible( + flex: 1, + child: Text( + overflow: TextOverflow.ellipsis, + value, + style: context + .textTheme.labelMedium?.toLight, + ), + ), + ] + ], + ); + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 4, + ), + ], + ), + ), + IconButton( + icon: Icon( + Icons.network_ping, + size: 20, + color: context.colorScheme.onSurfaceVariant, + ), + onPressed: () { + _delayTest(sortedProxies); + }, + ), + ], + ), + shape: const RoundedRectangleBorder( + side: BorderSide.none, + ), + collapsedShape: const RoundedRectangleBorder( + side: BorderSide.none, + ), + childrenPadding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 8, + right: 8, + ), + children: [ + Grid( + mainAxisSpacing: 8, + crossAxisSpacing: 8, + crossAxisCount: columns, + children: [ + for (final proxy in sortedProxies) + _currentProxyNameBuilder( + builder: (value) { + return ProxyCard( + style: CommonCardType.filled, + type: proxyCardType, + isSelected: value == proxy.name, + key: ValueKey('$groupName.${proxy.name}'), + proxy: proxy, + groupName: groupName, + ); + }, + ), + ], + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Selector2( + selector: (_, appState, config) { + final group = appState.getGroupWithName(groupName)!; + return ProxyGroupSelectorState( + proxyCardType: config.proxyCardType, + proxiesSortType: config.proxiesSortType, + columns: globalState.appController.columns, + sortNum: appState.sortNum, + proxies: group.all, + ); + }, + builder: (_, state, __) { + final proxies = state.proxies; + final columns = state.columns; + final proxyCardType = state.proxyCardType; + return switch (type) { + ProxiesType.tab => _buildTabGroupView( + proxies: proxies, + columns: columns, + proxyCardType: proxyCardType, + ), + ProxiesType.expansion => _buildExpansionGroupView( + proxies: proxies, + columns: columns, + proxyCardType: proxyCardType, + ), + }; + }, + ); + } +} + +class DelayTestButtonContainer extends StatefulWidget { + final Widget child; + final Future Function() onClick; + + const DelayTestButtonContainer({ + super.key, + required this.child, + required this.onClick, + }); + + @override + State createState() => + _DelayTestButtonContainerState(); +} + +class _DelayTestButtonContainerState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scale; + + _healthcheck() async { + _controller.forward(); + await widget.onClick(); + _controller.reverse(); + } + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration( + milliseconds: 200, + ), + ); + _scale = Tween( + begin: 1.0, + end: 0.0, + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval( + 0, + 1, + ), + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _controller.reverse(); + return FloatLayout( + floatingWidget: FloatWrapper( + child: AnimatedBuilder( + animation: _controller.view, + builder: (_, child) { + return SizedBox( + width: 56, + height: 56, + child: Transform.scale( + scale: _scale.value, + child: child, + ), + ); + }, + child: FloatingActionButton( + heroTag: null, + onPressed: _healthcheck, + child: const Icon(Icons.network_ping), + ), + ), + ), + child: widget.child, + ); + } +} + +class ProxyCard extends StatelessWidget { + final String groupName; + final Proxy proxy; + final bool isSelected; + final CommonCardType style; + final ProxyCardType type; + + const ProxyCard({ + super.key, + required this.groupName, + required this.proxy, + required this.isSelected, + this.style = CommonCardType.plain, + required this.type, + }); + + Measure get measure => globalState.appController.measure; + + Widget _buildDelayText() { + return SizedBox( + height: measure.labelSmallHeight, + child: Selector( + selector: (context, appState) => appState.getDelay( + proxy.name, + ), + builder: (context, delay, __) { + return FadeBox( + child: Builder( + builder: (_) { + if (delay == null) { + return Container(); + } + if (delay == 0) { + return SizedBox( + height: measure.labelSmallHeight, + width: measure.labelSmallHeight, + child: const CircularProgressIndicator( + strokeWidth: 2, + ), + ); + } + return Text( + delay > 0 ? '$delay ms' : "Timeout", + style: context.textTheme.labelSmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: other.getDelayColor( + delay, + ), + ), + ); + }, + ), + ); + }, + ), + ); + } + + Widget _buildProxyNameText(BuildContext context) { + return SizedBox( + height: measure.bodyMediumHeight * 2, + child: Text( + proxy.name, + maxLines: 2, + style: context.textTheme.bodyMedium?.copyWith( + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + + _changeProxy(BuildContext context) { + final appController = globalState.appController; + final group = appController.appState.getGroupWithName(groupName)!; + if (group.type != GroupType.Selector) { + globalState.showSnackBar( + context, + message: appLocalizations.notSelectedTip, + ); + return; + } + globalState.appController.config.updateCurrentSelectedMap( + groupName, + proxy.name, + ); + appController.changeProxy(); + } + + @override + Widget build(BuildContext context) { + final measure = globalState.appController.measure; + final delayText = _buildDelayText(); + final proxyNameText = _buildProxyNameText(context); + return CommonCard( + type: style, + key: key, + onPressed: () { + _changeProxy(context); + }, + isSelected: isSelected, + child: Container( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + proxyNameText, + const SizedBox( + height: 8, + ), + if (type == ProxyCardType.expand) ...[ + SizedBox( + height: measure.bodySmallHeight, + child: Selector( + selector: (context, appState) => appState.getDesc( + proxy.type, + proxy.name, + ), + builder: (_, desc, __) { + return TooltipText( + text: Text( + desc, + style: context.textTheme.bodySmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: context.textTheme.bodySmall?.color?.toLight(), + ), + ), + ); + }, + ), + ), + const SizedBox( + height: 8, + ), + delayText, + ] else + SizedBox( + height: measure.bodySmallHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + flex: 1, + child: TooltipText( + text: Text( + proxy.type, + style: context.textTheme.bodySmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: + context.textTheme.bodySmall?.color?.toLight(), + ), + ), + ), + ), + delayText, + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/fragments/proxies/expansion_panel.dart b/lib/fragments/proxies/expansion_panel.dart deleted file mode 100644 index 9e3af89..0000000 --- a/lib/fragments/proxies/expansion_panel.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/widgets/card.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class ProxiesExpansionPanelFragment extends StatefulWidget { - const ProxiesExpansionPanelFragment({super.key}); - - @override - State createState() => - _ProxiesExpansionPanelFragmentState(); -} - -class _ProxiesExpansionPanelFragmentState - extends State { - @override - Widget build(BuildContext context) { - return Selector2( - selector: (_, appState, config) { - final currentGroups = appState.currentGroups; - final groupNames = currentGroups.map((e) => e.name).toList(); - return ProxiesSelectorState( - groupNames: groupNames, - currentGroupName: config.currentGroupName, - ); - }, - builder: (_, state, __) { - return ListView.separated( - padding: const EdgeInsets.all(16), - itemCount: state.groupNames.length, - itemBuilder: (_, index) { - final groupName = state.groupNames[index]; - return CommonCard( - child: ExpansionTile( - title: Text(groupName), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(0.0), - side: const BorderSide(color: Colors.transparent), - ), - collapsedShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(0.0), - side: const BorderSide(color: Colors.transparent), - ), - children: [ - Text("1212313"), - ], - ), - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const SizedBox( - height: 16, - ); - }, - ); - }, - ); - } -} diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart deleted file mode 100644 index 64b418b..0000000 --- a/lib/fragments/proxies/proxies.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/fragments/proxies/tabview.dart'; -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class ProxiesFragment extends StatefulWidget { - const ProxiesFragment({super.key}); - - @override - State createState() => _ProxiesFragmentState(); -} - -class _ProxiesFragmentState extends State { - - _initActions() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final commonScaffoldState = - context.findAncestorStateOfType(); - final items = [ - CommonPopupMenuItem( - action: ProxiesSortType.none, - label: appLocalizations.defaultSort, - iconData: Icons.sort, - ), - CommonPopupMenuItem( - action: ProxiesSortType.delay, - label: appLocalizations.delaySort, - iconData: Icons.network_ping), - CommonPopupMenuItem( - action: ProxiesSortType.name, - label: appLocalizations.nameSort, - iconData: Icons.sort_by_alpha), - ]; - commonScaffoldState?.actions = [ - Selector( - selector: (_, config) => config.proxiesSortType, - builder: (_, proxiesSortType, __) { - return CommonPopupMenu.radio( - items: items, - onSelected: (value) { - final config = context.read(); - config.proxiesSortType = value; - }, - selectedValue: proxiesSortType, - ); - }, - ), - const SizedBox( - width: 8, - ) - ]; - }); - } - - @override - Widget build(BuildContext context) { - return Selector( - selector: (_, appState) => appState.currentLabel == 'proxies', - builder: (_, isCurrent, child) { - if (isCurrent) { - _initActions(); - } - return child!; - }, - child: const ProxiesTabFragment(), - ); - } -} - diff --git a/lib/fragments/proxies/tabview.dart b/lib/fragments/proxies/tabview.dart deleted file mode 100644 index 03484bf..0000000 --- a/lib/fragments/proxies/tabview.dart +++ /dev/null @@ -1,470 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:fl_clash/clash/clash.dart'; -import 'package:fl_clash/common/common.dart'; -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/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class ProxiesTabFragment extends StatefulWidget { - const ProxiesTabFragment({super.key}); - - @override - State createState() => _ProxiesTabFragmentState(); -} - -class _ProxiesTabFragmentState extends State with TickerProviderStateMixin { - TabController? _tabController; - - _handleTabControllerChange() { - final indexIsChanging = _tabController?.indexIsChanging ?? false; - if (indexIsChanging) return; - final index = _tabController?.index; - if (index == null) return; - final appController = globalState.appController; - final currentGroups = appController.appState.currentGroups; - if (currentGroups.length > index) { - appController.config.updateCurrentGroupName(currentGroups[index].name); - } - } - - @override - void dispose() { - super.dispose(); - _tabController?.dispose(); - } - - @override - Widget build(BuildContext context) { - return Selector2( - selector: (_, appState, config) { - final currentGroups = appState.currentGroups; - final groupNames = currentGroups.map((e) => e.name).toList(); - return ProxiesSelectorState( - groupNames: groupNames, - currentGroupName: config.currentGroupName, - ); - }, - shouldRebuild: (prev, next) { - if (!const ListEquality() - .equals(prev.groupNames, next.groupNames)) { - _tabController?.removeListener(_handleTabControllerChange); - _tabController?.dispose(); - _tabController = null; - return true; - } - return false; - }, - builder: (_, state, __) { - final index = state.groupNames.indexWhere( - (item) => item == state.currentGroupName, - ); - _tabController ??= TabController( - length: state.groupNames.length, - initialIndex: index == -1 ? 0 : index, - vsync: this, - )..addListener(_handleTabControllerChange); - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TabBar( - controller: _tabController, - padding: const EdgeInsets.symmetric(horizontal: 16), - dividerColor: Colors.transparent, - isScrollable: true, - tabAlignment: TabAlignment.start, - overlayColor: - const WidgetStatePropertyAll(Colors.transparent), - tabs: [ - for (final groupName in state.groupNames) - Tab( - text: groupName, - ), - ], - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - for (final groupName in state.groupNames) - KeepContainer( - key: ObjectKey(groupName), - child: ProxiesTabView( - groupName: groupName, - ), - ), - ], - ), - ) - ], - ); - }, - ); - } -} - -class ProxiesTabView extends StatelessWidget { - final String groupName; - - const ProxiesTabView({ - super.key, - required this.groupName, - }); - - List _sortOfName(List proxies) { - return List.of(proxies) - ..sort( - (a, b) => other.sortByChar(a.name, b.name), - ); - } - - List _sortOfDelay(BuildContext context, List proxies) { - final appState = context.read(); - return proxies = List.of(proxies) - ..sort( - (a, b) { - final aDelay = appState.getDelay(a.name); - final bDelay = appState.getDelay(b.name); - if (aDelay == null && bDelay == null) { - return 0; - } - if (aDelay == null || aDelay == -1) { - return 1; - } - if (bDelay == null || bDelay == -1) { - return -1; - } - return aDelay.compareTo(bDelay); - }, - ); - } - - _getProxies( - BuildContext context, - List proxies, - ProxiesSortType proxiesSortType, - ) { - if (proxiesSortType == ProxiesSortType.delay) { - return _sortOfDelay(context, proxies); - } - if (proxiesSortType == ProxiesSortType.name) return _sortOfName(proxies); - return proxies; - } - - double _getItemHeight(BuildContext context) { - final measure = globalState.appController.measure; - return 12 * 2 + - measure.bodyMediumHeight * 2 + - measure.bodySmallHeight + - measure.labelSmallHeight + - 8 * 2; - } - - int _getColumns(ViewMode viewMode) { - switch (viewMode) { - case ViewMode.mobile: - return 2; - case ViewMode.laptop: - return 3; - case ViewMode.desktop: - return 4; - } - } - - _delayTest(List 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: proxyName, - value: 0, - ), - ); - clashCore.getDelay(proxyName).then((delay) { - globalState.appController.setDelay(delay); - }); - } - await Future.delayed(httpTimeoutDuration + moreDuration); - } - - @override - Widget build(BuildContext context) { - return Selector2( - selector: (_, appState, config) { - return ProxiesTabViewSelectorState( - proxiesSortType: config.proxiesSortType, - sortNum: appState.sortNum, - group: appState.getGroupWithName(groupName)!, - viewMode: appState.viewMode, - ); - }, - builder: (_, state, __) { - final proxies = _getProxies( - context, - state.group.all, - state.proxiesSortType, - ); - return DelayTestButtonContainer( - onClick: () async { - await _delayTest( - state.group.all, - ); - }, - child: Align( - alignment: Alignment.topCenter, - child: GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: _getColumns(state.viewMode), - mainAxisSpacing: 8, - crossAxisSpacing: 8, - mainAxisExtent: _getItemHeight(context), - ), - itemCount: proxies.length, - itemBuilder: (_, index) { - final proxy = proxies[index]; - return ProxyCard( - key: ValueKey('$groupName.${proxy.name}'), - proxy: proxy, - groupName: groupName, - ); - }, - ), - ), - ); - }, - ); - } -} - -class ProxyCard extends StatelessWidget { - final String groupName; - final Proxy proxy; - - const ProxyCard({ - super.key, - required this.groupName, - required this.proxy, - }); - - @override - Widget build(BuildContext context) { - final measure = globalState.appController.measure; - return Selector3( - selector: (_, appState, config, clashConfig) { - final group = appState.getGroupWithName(groupName)!; - bool isSelected = config.currentSelectedMap[group.name] == proxy.name || - (config.currentSelectedMap[group.name] == null && - group.now == proxy.name); - return ProxiesCardSelectorState( - isSelected: isSelected, - ); - }, - builder: (_, state, __) { - return CommonCard( - isSelected: state.isSelected, - onPressed: () { - final appController = globalState.appController; - final group = appController.appState.getGroupWithName(groupName)!; - if (group.type != GroupType.Selector) { - globalState.showSnackBar( - context, - message: appLocalizations.notSelectedTip, - ); - return; - } - globalState.appController.config.updateCurrentSelectedMap( - groupName, - proxy.name, - ); - globalState.appController.changeProxy(); - }, - selectWidget: Container( - alignment: Alignment.topRight, - margin: const EdgeInsets.all(8), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: const SelectIcon(), - ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: measure.bodyMediumHeight * 2, - child: Text( - proxy.name, - maxLines: 2, - style: context.textTheme.bodyMedium?.copyWith( - overflow: TextOverflow.ellipsis, - ), - ), - ), - const SizedBox( - height: 8, - ), - SizedBox( - height: measure.bodySmallHeight, - child: Selector( - selector: (context, appState) => appState.getDesc( - proxy.type, - proxy.name, - ), - builder: (_, desc, __) { - return TooltipText( - text: Text( - desc, - style: context.textTheme.bodySmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: - context.textTheme.bodySmall?.color?.toLight(), - ), - ), - ); - }, - ), - ), - const SizedBox( - height: 8, - ), - SizedBox( - height: measure.labelSmallHeight, - child: Selector( - selector: (context, appState) => appState.getDelay( - proxy.name, - ), - builder: (_, delay, __) { - return FadeBox( - child: Builder( - builder: (_) { - if (delay == null) { - return Container(); - } - if (delay == 0) { - return SizedBox( - height: measure.labelSmallHeight, - width: measure.labelSmallHeight, - child: const CircularProgressIndicator( - strokeWidth: 2, - ), - ); - } - return Text( - delay > 0 ? '$delay ms' : "Timeout", - style: context.textTheme.labelSmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: other.getDelayColor( - delay, - ), - ), - ); - }, - ), - ); - }, - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -class DelayTestButtonContainer extends StatefulWidget { - final Widget child; - final Future Function() onClick; - - const DelayTestButtonContainer({ - super.key, - required this.child, - required this.onClick, - }); - - @override - State createState() => - _DelayTestButtonContainerState(); -} - -class _DelayTestButtonContainerState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _scale; - - _healthcheck() async { - _controller.forward(); - await widget.onClick(); - _controller.reverse(); - } - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration( - milliseconds: 200, - ), - ); - _scale = Tween( - begin: 1.0, - end: 0.0, - ).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval( - 0, - 1, - ), - ), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - _controller.reverse(); - return FloatLayout( - floatingWidget: FloatWrapper( - child: AnimatedBuilder( - animation: _controller, - builder: (_, child) { - return SizedBox( - width: 56, - height: 56, - child: Transform.scale( - scale: _scale.value, - child: child, - ), - ); - }, - child: FloatingActionButton( - heroTag: null, - onPressed: _healthcheck, - child: const Icon(Icons.network_ping), - ), - ), - ), - child: widget.child, - ); - } -} diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 92bf0d0..861b015 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -111,7 +111,7 @@ "noMoreInfoDesc": "No more info", "profileParseErrorDesc": "profile parse error", "proxyPort": "ProxyPort", - "proxyPortDesc": "Set the clash listening port", + "proxyPortDesc": "Set the Clash listening port", "port": "Port", "logLevel": "LogLevel", "show": "Show", @@ -166,8 +166,8 @@ "allowBypass": "Allow applications to bypass VPN", "allowBypassDesc": "Some apps can bypass VPN when turned on", "externalController": "ExternalController", - "externalControllerDesc": "Once enabled, the clash kernel can be controlled on port 9090", - "ipv6Desc": "When turned on it will be able to receive ipv6 traffic", + "externalControllerDesc": "Once enabled, the Clash kernel can be controlled on port 9090", + "ipv6Desc": "When turned on it will be able to receive IPv6 traffic", "app": "App", "general": "General", "systemProxyDesc": "Attach HTTP proxy to VpnService", diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index 656a33e..8577899 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -111,7 +111,7 @@ "noMoreInfoDesc": "暂无更多信息", "profileParseErrorDesc": "配置文件解析错误", "proxyPort": "代理端口", - "proxyPortDesc": "设置clash监听端口", + "proxyPortDesc": "设置Clash监听端口", "port": "端口", "logLevel": "日志等级", "show": "显示", @@ -163,11 +163,11 @@ "checkError": "检测失败", "ipCheckTimeout": "Ip检测超时", "search": "搜索", - "allowBypass": "允许应用绕过vpn", + "allowBypass": "允许应用绕过VPN", "allowBypassDesc": "开启后部分应用可绕过VPN", "externalController": "外部控制器", - "externalControllerDesc": "开启后将可以通过9090端口控制clash内核", - "ipv6Desc": "开启后将可以接收ipv6流量", + "externalControllerDesc": "开启后将可以通过9090端口控制Clash内核", + "ipv6Desc": "开启后将可以接收IPv6流量", "app": "应用", "general": "基础", "systemProxyDesc": "为VpnService附加HTTP代理", diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index a073ec8..fffebeb 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -127,7 +127,7 @@ class MessageLookup extends MessageLookupByLibrary { "externalController": MessageLookupByLibrary.simpleMessage("ExternalController"), "externalControllerDesc": MessageLookupByLibrary.simpleMessage( - "Once enabled, the clash kernel can be controlled on port 9090"), + "Once enabled, the Clash kernel can be controlled on port 9090"), "externalResources": MessageLookupByLibrary.simpleMessage("External resources"), "file": MessageLookupByLibrary.simpleMessage("File"), @@ -156,7 +156,7 @@ class MessageLookup extends MessageLookupByLibrary { "ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip check timeout"), "ipv6Desc": MessageLookupByLibrary.simpleMessage( - "When turned on it will be able to receive ipv6 traffic"), + "When turned on it will be able to receive IPv6 traffic"), "just": MessageLookupByLibrary.simpleMessage("Just"), "language": MessageLookupByLibrary.simpleMessage("Language"), "light": MessageLookupByLibrary.simpleMessage("Light"), @@ -230,7 +230,7 @@ class MessageLookup extends MessageLookupByLibrary { "proxies": MessageLookupByLibrary.simpleMessage("Proxies"), "proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage( - "Set the clash listening port"), + "Set the Clash listening port"), "qrcode": MessageLookupByLibrary.simpleMessage("QR code"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage( "Scan QR code to obtain profile"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 4cb5999..6a51e62 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -36,7 +36,7 @@ class MessageLookup extends MessageLookupByLibrary { "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "ago": MessageLookupByLibrary.simpleMessage("前"), - "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过vpn"), + "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), @@ -104,7 +104,7 @@ class MessageLookup extends MessageLookupByLibrary { "expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制器"), "externalControllerDesc": - MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制clash内核"), + MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"), "externalResources": MessageLookupByLibrary.simpleMessage("外部资源"), "file": MessageLookupByLibrary.simpleMessage("文件"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), @@ -125,7 +125,7 @@ class MessageLookup extends MessageLookupByLibrary { "init": MessageLookupByLibrary.simpleMessage("初始化"), "intranetIp": MessageLookupByLibrary.simpleMessage("内网 IP"), "ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"), - "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"), + "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "just": MessageLookupByLibrary.simpleMessage("刚刚"), "language": MessageLookupByLibrary.simpleMessage("语言"), "light": MessageLookupByLibrary.simpleMessage("浅色"), @@ -186,7 +186,7 @@ class MessageLookup extends MessageLookupByLibrary { "project": MessageLookupByLibrary.simpleMessage("项目"), "proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), - "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置clash监听端口"), + "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index afa6069..b20af82 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1170,10 +1170,10 @@ class AppLocalizations { ); } - /// `Set the clash listening port` + /// `Set the Clash listening port` String get proxyPortDesc { return Intl.message( - 'Set the clash listening port', + 'Set the Clash listening port', name: 'proxyPortDesc', desc: '', args: [], @@ -1720,20 +1720,20 @@ class AppLocalizations { ); } - /// `Once enabled, the clash kernel can be controlled on port 9090` + /// `Once enabled, the Clash kernel can be controlled on port 9090` String get externalControllerDesc { return Intl.message( - 'Once enabled, the clash kernel can be controlled on port 9090', + 'Once enabled, the Clash kernel can be controlled on port 9090', name: 'externalControllerDesc', desc: '', args: [], ); } - /// `When turned on it will be able to receive ipv6 traffic` + /// `When turned on it will be able to receive IPv6 traffic` String get ipv6Desc { return Intl.message( - 'When turned on it will be able to receive ipv6 traffic', + 'When turned on it will be able to receive IPv6 traffic', name: 'ipv6Desc', desc: '', args: [], diff --git a/lib/models/config.dart b/lib/models/config.dart index def82b3..f94c95b 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -56,6 +57,9 @@ class Config extends ChangeNotifier { bool _allowBypass; bool _systemProxy; DAV? _dav; + ProxiesType _proxiesType; + ProxyCardType _proxyCardType; + int _proxiesColumns; Config() : _profiles = [], @@ -73,7 +77,10 @@ class Config extends ChangeNotifier { _systemProxy = true, _accessControl = const AccessControl(), _isAnimateToPage = true, - _allowBypass = true; + _allowBypass = true, + _proxyCardType = ProxyCardType.expand, + _proxiesType = ProxiesType.tab, + _proxiesColumns = 2; deleteProfileById(String id) { _profiles = profiles.where((element) => element.id != id).toList(); @@ -150,6 +157,19 @@ class Config extends ChangeNotifier { String? get currentGroupName => currentProfile?.currentGroupName; + Set get currentUnfoldSet => currentProfile?.unfoldSet ?? {}; + + updateCurrentUnfoldSet(Set value) { + if (!const SetEquality().equals(currentUnfoldSet, value)) { + _setProfile( + currentProfile!.copyWith( + unfoldSet: value, + ), + ); + notifyListeners(); + } + } + updateCurrentGroupName(String groupName) { if (currentProfile != null && currentProfile!.currentGroupName != groupName) { @@ -364,6 +384,36 @@ class Config extends ChangeNotifier { } } + @JsonKey(defaultValue: ProxiesType.tab) + ProxiesType get proxiesType => _proxiesType; + + set proxiesType(ProxiesType value) { + if (_proxiesType != value) { + _proxiesType = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: ProxyCardType.expand) + ProxyCardType get proxyCardType => _proxyCardType; + + set proxyCardType(ProxyCardType value) { + if (_proxyCardType != value) { + _proxyCardType = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: 2) + int get proxiesColumns => _proxiesColumns; + + set proxiesColumns(int value) { + if (_proxiesColumns != value) { + _proxiesColumns = value; + notifyListeners(); + } + } + update([ Config? config, RecoveryOption recoveryOptions = RecoveryOption.all, @@ -383,6 +433,7 @@ class Config extends ChangeNotifier { _autoLaunch = config._autoLaunch; _silentLaunch = config._silentLaunch; _autoRun = config._autoRun; + _proxiesType = config._proxiesType; _openLog = config._openLog; _themeMode = config._themeMode; _locale = config._locale; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 6d3743d..7fd3621 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -34,7 +34,14 @@ Config _$ConfigFromJson(Map json) => Config() ..isCompatible = json['isCompatible'] as bool? ?? true ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true ..allowBypass = json['allowBypass'] as bool? ?? true - ..systemProxy = json['systemProxy'] as bool? ?? true; + ..systemProxy = json['systemProxy'] as bool? ?? true + ..proxiesType = + $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType']) ?? + ProxiesType.tab + ..proxyCardType = + $enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ?? + ProxyCardType.expand + ..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2; Map _$ConfigToJson(Config instance) => { 'profiles': instance.profiles, @@ -56,6 +63,9 @@ Map _$ConfigToJson(Config instance) => { 'autoCheckUpdate': instance.autoCheckUpdate, 'allowBypass': instance.allowBypass, 'systemProxy': instance.systemProxy, + 'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!, + 'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!, + 'proxiesColumns': instance.proxiesColumns, }; const _$ThemeModeEnumMap = { @@ -70,6 +80,16 @@ const _$ProxiesSortTypeEnumMap = { ProxiesSortType.name: 'name', }; +const _$ProxiesTypeEnumMap = { + ProxiesType.tab: 'tab', + ProxiesType.expansion: 'expansion', +}; + +const _$ProxyCardTypeEnumMap = { + ProxyCardType.expand: 'expand', + ProxyCardType.shrink: 'shrink', +}; + _$AccessControlImpl _$$AccessControlImplFromJson(Map json) => _$AccessControlImpl( mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ?? diff --git a/lib/models/generated/profile.freezed.dart b/lib/models/generated/profile.freezed.dart index 4e6d2ed..7d45b1b 100644 --- a/lib/models/generated/profile.freezed.dart +++ b/lib/models/generated/profile.freezed.dart @@ -222,6 +222,7 @@ mixin _$Profile { UserInfo? get userInfo => throw _privateConstructorUsedError; bool get autoUpdate => throw _privateConstructorUsedError; Map get selectedMap => throw _privateConstructorUsedError; + Set get unfoldSet => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -242,7 +243,8 @@ abstract class $ProfileCopyWith<$Res> { Duration autoUpdateDuration, UserInfo? userInfo, bool autoUpdate, - Map selectedMap}); + Map selectedMap, + Set unfoldSet}); $UserInfoCopyWith<$Res>? get userInfo; } @@ -269,6 +271,7 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> Object? userInfo = freezed, Object? autoUpdate = null, Object? selectedMap = null, + Object? unfoldSet = null, }) { return _then(_value.copyWith( id: null == id @@ -307,6 +310,10 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> ? _value.selectedMap : selectedMap // ignore: cast_nullable_to_non_nullable as Map, + unfoldSet: null == unfoldSet + ? _value.unfoldSet + : unfoldSet // ignore: cast_nullable_to_non_nullable + as Set, ) as $Val); } @@ -339,7 +346,8 @@ abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> { Duration autoUpdateDuration, UserInfo? userInfo, bool autoUpdate, - Map selectedMap}); + Map selectedMap, + Set unfoldSet}); @override $UserInfoCopyWith<$Res>? get userInfo; @@ -365,6 +373,7 @@ class __$$ProfileImplCopyWithImpl<$Res> Object? userInfo = freezed, Object? autoUpdate = null, Object? selectedMap = null, + Object? unfoldSet = null, }) { return _then(_$ProfileImpl( id: null == id @@ -403,6 +412,10 @@ class __$$ProfileImplCopyWithImpl<$Res> ? _value._selectedMap : selectedMap // ignore: cast_nullable_to_non_nullable as Map, + unfoldSet: null == unfoldSet + ? _value._unfoldSet + : unfoldSet // ignore: cast_nullable_to_non_nullable + as Set, )); } } @@ -419,8 +432,10 @@ class _$ProfileImpl implements _Profile { required this.autoUpdateDuration, this.userInfo, this.autoUpdate = true, - final Map selectedMap = const {}}) - : _selectedMap = selectedMap; + final Map selectedMap = const {}, + final Set unfoldSet = const {}}) + : _selectedMap = selectedMap, + _unfoldSet = unfoldSet; factory _$ProfileImpl.fromJson(Map json) => _$$ProfileImplFromJson(json); @@ -452,9 +467,18 @@ class _$ProfileImpl implements _Profile { return EqualUnmodifiableMapView(_selectedMap); } + final Set _unfoldSet; + @override + @JsonKey() + Set get unfoldSet { + if (_unfoldSet is EqualUnmodifiableSetView) return _unfoldSet; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_unfoldSet); + } + @override String toString() { - return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap)'; + return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet)'; } @override @@ -476,7 +500,9 @@ class _$ProfileImpl implements _Profile { (identical(other.autoUpdate, autoUpdate) || other.autoUpdate == autoUpdate) && const DeepCollectionEquality() - .equals(other._selectedMap, _selectedMap)); + .equals(other._selectedMap, _selectedMap) && + const DeepCollectionEquality() + .equals(other._unfoldSet, _unfoldSet)); } @JsonKey(ignore: true) @@ -491,7 +517,8 @@ class _$ProfileImpl implements _Profile { autoUpdateDuration, userInfo, autoUpdate, - const DeepCollectionEquality().hash(_selectedMap)); + const DeepCollectionEquality().hash(_selectedMap), + const DeepCollectionEquality().hash(_unfoldSet)); @JsonKey(ignore: true) @override @@ -517,7 +544,8 @@ abstract class _Profile implements Profile { required final Duration autoUpdateDuration, final UserInfo? userInfo, final bool autoUpdate, - final Map selectedMap}) = _$ProfileImpl; + final Map selectedMap, + final Set unfoldSet}) = _$ProfileImpl; factory _Profile.fromJson(Map json) = _$ProfileImpl.fromJson; @@ -540,6 +568,8 @@ abstract class _Profile implements Profile { @override Map get selectedMap; @override + Set get unfoldSet; + @override @JsonKey(ignore: true) _$$ProfileImplCopyWith<_$ProfileImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/generated/profile.g.dart b/lib/models/generated/profile.g.dart index 7d7f207..5741d26 100644 --- a/lib/models/generated/profile.g.dart +++ b/lib/models/generated/profile.g.dart @@ -41,6 +41,10 @@ _$ProfileImpl _$$ProfileImplFromJson(Map json) => (k, e) => MapEntry(k, e as String), ) ?? const {}, + unfoldSet: (json['unfoldSet'] as List?) + ?.map((e) => e as String) + .toSet() ?? + const {}, ); Map _$$ProfileImplToJson(_$ProfileImpl instance) => @@ -54,4 +58,5 @@ Map _$$ProfileImplToJson(_$ProfileImpl instance) => 'userInfo': instance.userInfo, 'autoUpdate': instance.autoUpdate, 'selectedMap': instance.selectedMap, + 'unfoldSet': instance.unfoldSet.toList(), }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index bcd6271..5939154 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -1730,39 +1730,37 @@ abstract class _ProxiesSelectorState implements ProxiesSelectorState { } /// @nodoc -mixin _$ProxiesTabViewSelectorState { +mixin _$ProxyGroupSelectorState { ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError; + ProxyCardType get proxyCardType => throw _privateConstructorUsedError; num get sortNum => throw _privateConstructorUsedError; - Group get group => throw _privateConstructorUsedError; - ViewMode get viewMode => throw _privateConstructorUsedError; + List get proxies => throw _privateConstructorUsedError; + int get columns => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $ProxiesTabViewSelectorStateCopyWith - get copyWith => throw _privateConstructorUsedError; + $ProxyGroupSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -abstract class $ProxiesTabViewSelectorStateCopyWith<$Res> { - factory $ProxiesTabViewSelectorStateCopyWith( - ProxiesTabViewSelectorState value, - $Res Function(ProxiesTabViewSelectorState) then) = - _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, - ProxiesTabViewSelectorState>; +abstract class $ProxyGroupSelectorStateCopyWith<$Res> { + factory $ProxyGroupSelectorStateCopyWith(ProxyGroupSelectorState value, + $Res Function(ProxyGroupSelectorState) then) = + _$ProxyGroupSelectorStateCopyWithImpl<$Res, ProxyGroupSelectorState>; @useResult $Res call( {ProxiesSortType proxiesSortType, + ProxyCardType proxyCardType, num sortNum, - Group group, - ViewMode viewMode}); - - $GroupCopyWith<$Res> get group; + List proxies, + int columns}); } /// @nodoc -class _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, - $Val extends ProxiesTabViewSelectorState> - implements $ProxiesTabViewSelectorStateCopyWith<$Res> { - _$ProxiesTabViewSelectorStateCopyWithImpl(this._value, this._then); +class _$ProxyGroupSelectorStateCopyWithImpl<$Res, + $Val extends ProxyGroupSelectorState> + implements $ProxyGroupSelectorStateCopyWith<$Res> { + _$ProxyGroupSelectorStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -1773,165 +1771,177 @@ class _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, @override $Res call({ Object? proxiesSortType = null, + Object? proxyCardType = null, Object? sortNum = null, - Object? group = null, - Object? viewMode = null, + Object? proxies = null, + Object? columns = null, }) { return _then(_value.copyWith( proxiesSortType: null == proxiesSortType ? _value.proxiesSortType : proxiesSortType // ignore: cast_nullable_to_non_nullable as ProxiesSortType, + proxyCardType: null == proxyCardType + ? _value.proxyCardType + : proxyCardType // ignore: cast_nullable_to_non_nullable + as ProxyCardType, sortNum: null == sortNum ? _value.sortNum : sortNum // ignore: cast_nullable_to_non_nullable as num, - group: null == group - ? _value.group - : group // ignore: cast_nullable_to_non_nullable - as Group, - viewMode: null == viewMode - ? _value.viewMode - : viewMode // ignore: cast_nullable_to_non_nullable - as ViewMode, + proxies: null == proxies + ? _value.proxies + : proxies // ignore: cast_nullable_to_non_nullable + as List, + columns: null == columns + ? _value.columns + : columns // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } - - @override - @pragma('vm:prefer-inline') - $GroupCopyWith<$Res> get group { - return $GroupCopyWith<$Res>(_value.group, (value) { - return _then(_value.copyWith(group: value) as $Val); - }); - } } /// @nodoc -abstract class _$$ProxiesTabViewSelectorStateImplCopyWith<$Res> - implements $ProxiesTabViewSelectorStateCopyWith<$Res> { - factory _$$ProxiesTabViewSelectorStateImplCopyWith( - _$ProxiesTabViewSelectorStateImpl value, - $Res Function(_$ProxiesTabViewSelectorStateImpl) then) = - __$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res>; +abstract class _$$ProxyGroupSelectorStateImplCopyWith<$Res> + implements $ProxyGroupSelectorStateCopyWith<$Res> { + factory _$$ProxyGroupSelectorStateImplCopyWith( + _$ProxyGroupSelectorStateImpl value, + $Res Function(_$ProxyGroupSelectorStateImpl) then) = + __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>; @override @useResult $Res call( {ProxiesSortType proxiesSortType, + ProxyCardType proxyCardType, num sortNum, - Group group, - ViewMode viewMode}); - - @override - $GroupCopyWith<$Res> get group; + List proxies, + int columns}); } /// @nodoc -class __$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res> - extends _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, - _$ProxiesTabViewSelectorStateImpl> - implements _$$ProxiesTabViewSelectorStateImplCopyWith<$Res> { - __$$ProxiesTabViewSelectorStateImplCopyWithImpl( - _$ProxiesTabViewSelectorStateImpl _value, - $Res Function(_$ProxiesTabViewSelectorStateImpl) _then) +class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res> + extends _$ProxyGroupSelectorStateCopyWithImpl<$Res, + _$ProxyGroupSelectorStateImpl> + implements _$$ProxyGroupSelectorStateImplCopyWith<$Res> { + __$$ProxyGroupSelectorStateImplCopyWithImpl( + _$ProxyGroupSelectorStateImpl _value, + $Res Function(_$ProxyGroupSelectorStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ Object? proxiesSortType = null, + Object? proxyCardType = null, Object? sortNum = null, - Object? group = null, - Object? viewMode = null, + Object? proxies = null, + Object? columns = null, }) { - return _then(_$ProxiesTabViewSelectorStateImpl( + return _then(_$ProxyGroupSelectorStateImpl( proxiesSortType: null == proxiesSortType ? _value.proxiesSortType : proxiesSortType // ignore: cast_nullable_to_non_nullable as ProxiesSortType, + proxyCardType: null == proxyCardType + ? _value.proxyCardType + : proxyCardType // ignore: cast_nullable_to_non_nullable + as ProxyCardType, sortNum: null == sortNum ? _value.sortNum : sortNum // ignore: cast_nullable_to_non_nullable as num, - group: null == group - ? _value.group - : group // ignore: cast_nullable_to_non_nullable - as Group, - viewMode: null == viewMode - ? _value.viewMode - : viewMode // ignore: cast_nullable_to_non_nullable - as ViewMode, + proxies: null == proxies + ? _value._proxies + : proxies // ignore: cast_nullable_to_non_nullable + as List, + columns: null == columns + ? _value.columns + : columns // ignore: cast_nullable_to_non_nullable + as int, )); } } /// @nodoc -class _$ProxiesTabViewSelectorStateImpl - implements _ProxiesTabViewSelectorState { - const _$ProxiesTabViewSelectorStateImpl( +class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState { + const _$ProxyGroupSelectorStateImpl( {required this.proxiesSortType, + required this.proxyCardType, required this.sortNum, - required this.group, - required this.viewMode}); + required final List proxies, + required this.columns}) + : _proxies = proxies; @override final ProxiesSortType proxiesSortType; @override + final ProxyCardType proxyCardType; + @override final num sortNum; + final List _proxies; @override - final Group group; + List get proxies { + if (_proxies is EqualUnmodifiableListView) return _proxies; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_proxies); + } + @override - final ViewMode viewMode; + final int columns; @override String toString() { - return 'ProxiesTabViewSelectorState(proxiesSortType: $proxiesSortType, sortNum: $sortNum, group: $group, viewMode: $viewMode)'; + return 'ProxyGroupSelectorState(proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, proxies: $proxies, columns: $columns)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ProxiesTabViewSelectorStateImpl && + other is _$ProxyGroupSelectorStateImpl && (identical(other.proxiesSortType, proxiesSortType) || other.proxiesSortType == proxiesSortType) && + (identical(other.proxyCardType, proxyCardType) || + other.proxyCardType == proxyCardType) && (identical(other.sortNum, sortNum) || other.sortNum == sortNum) && - (identical(other.group, group) || other.group == group) && - (identical(other.viewMode, viewMode) || - other.viewMode == viewMode)); + const DeepCollectionEquality().equals(other._proxies, _proxies) && + (identical(other.columns, columns) || other.columns == columns)); } @override - int get hashCode => - Object.hash(runtimeType, proxiesSortType, sortNum, group, viewMode); + int get hashCode => Object.hash(runtimeType, proxiesSortType, proxyCardType, + sortNum, const DeepCollectionEquality().hash(_proxies), columns); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl> - get copyWith => __$$ProxiesTabViewSelectorStateImplCopyWithImpl< - _$ProxiesTabViewSelectorStateImpl>(this, _$identity); + _$$ProxyGroupSelectorStateImplCopyWith<_$ProxyGroupSelectorStateImpl> + get copyWith => __$$ProxyGroupSelectorStateImplCopyWithImpl< + _$ProxyGroupSelectorStateImpl>(this, _$identity); } -abstract class _ProxiesTabViewSelectorState - implements ProxiesTabViewSelectorState { - const factory _ProxiesTabViewSelectorState( +abstract class _ProxyGroupSelectorState implements ProxyGroupSelectorState { + const factory _ProxyGroupSelectorState( {required final ProxiesSortType proxiesSortType, + required final ProxyCardType proxyCardType, required final num sortNum, - required final Group group, - required final ViewMode viewMode}) = _$ProxiesTabViewSelectorStateImpl; + required final List proxies, + required final int columns}) = _$ProxyGroupSelectorStateImpl; @override ProxiesSortType get proxiesSortType; @override + ProxyCardType get proxyCardType; + @override num get sortNum; @override - Group get group; + List get proxies; @override - ViewMode get viewMode; + int get columns; @override @JsonKey(ignore: true) - _$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl> + _$$ProxyGroupSelectorStateImplCopyWith<_$ProxyGroupSelectorStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 435fd04..1825f4c 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -54,6 +54,7 @@ class Profile with _$Profile { UserInfo? userInfo, @Default(true) bool autoUpdate, @Default({}) SelectedMap selectedMap, + @Default({}) Set unfoldSet, }) = _Profile; factory Profile.fromJson(Map json) => diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 06b58f7..eb817cb 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -99,13 +99,14 @@ class ProxiesSelectorState with _$ProxiesSelectorState { } @freezed -class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState { - const factory ProxiesTabViewSelectorState({ +class ProxyGroupSelectorState with _$ProxyGroupSelectorState { + const factory ProxyGroupSelectorState({ required ProxiesSortType proxiesSortType, + required ProxyCardType proxyCardType, required num sortNum, - required Group group, - required ViewMode viewMode, - }) = _ProxiesTabViewSelectorState; + required List proxies, + required int columns, + }) = _ProxyGroupSelectorState; } @freezed diff --git a/lib/state.dart b/lib/state.dart index 48f06eb..d00324c 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:animations/animations.dart'; import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; @@ -261,6 +262,18 @@ class GlobalState { return null; } } + + int getColumns(ViewMode viewMode,int currentColumns){ + final targetColumnsArray = switch (viewMode) { + ViewMode.mobile => [2, 1], + ViewMode.laptop => [3, 2], + ViewMode.desktop => [4, 3], + }; + if (targetColumnsArray.contains(currentColumns)) { + return currentColumns; + } + return targetColumnsArray.first; + } } final globalState = GlobalState(); diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index 7dd823b..becfdc3 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -1,4 +1,5 @@ import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; import 'text.dart'; @@ -54,6 +55,7 @@ class CommonCard extends StatelessWidget { const CommonCard({ super.key, bool? isSelected, + this.type = CommonCardType.plain, this.onPressed, this.info, this.selectWidget, @@ -65,10 +67,14 @@ class CommonCard extends StatelessWidget { final Widget? selectWidget; final Widget child; final Info? info; + final CommonCardType type; BorderSide getBorderSide(BuildContext context, Set states) { + if(type == CommonCardType.filled){ + return BorderSide.none; + } final colorScheme = Theme.of(context).colorScheme; - var hoverColor = isSelected + final hoverColor = isSelected ? colorScheme.primary.toLight() : colorScheme.primary.toLighter(); if (states.contains(WidgetState.hovered) || @@ -86,17 +92,28 @@ class CommonCard extends StatelessWidget { Color? getBackgroundColor(BuildContext context, Set states) { final colorScheme = Theme.of(context).colorScheme; - if (isSelected) { - return colorScheme.secondaryContainer; + switch(type){ + case CommonCardType.plain: + if (isSelected) { + return colorScheme.secondaryContainer; + } + if (states.isEmpty) { + return colorScheme.secondaryContainer.toLittle(); + } + return Theme.of(context) + .outlinedButtonTheme + .style + ?.backgroundColor + ?.resolve(states); + case CommonCardType.filled: + if (isSelected) { + return colorScheme.secondaryContainer; + } + if (states.isEmpty) { + return colorScheme.surfaceContainerLow; + } + return colorScheme.surfaceContainer; } - if (states.isEmpty) { - return colorScheme.secondaryContainer.toLittle(); - } - return Theme.of(context) - .outlinedButtonTheme - .style - ?.backgroundColor - ?.resolve(states); } @override @@ -136,11 +153,7 @@ class CommonCard extends StatelessWidget { (states) => getBorderSide(context, states), ), ), - onPressed: () { - if (onPressed != null) { - onPressed!(); - } - }, + onPressed: onPressed, child: Builder( builder: (_) { List children = []; diff --git a/lib/widgets/chip.dart b/lib/widgets/chip.dart index 4d65277..ae564a2 100644 --- a/lib/widgets/chip.dart +++ b/lib/widgets/chip.dart @@ -17,12 +17,17 @@ class CommonChip extends StatelessWidget { @override Widget build(BuildContext context) { - if(type == ChipType.delete){ + if (type == ChipType.delete) { return Chip( avatar: avatar, + padding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 4, + ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, onDeleted: onPressed ?? () {}, - side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), + side: + BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), labelStyle: Theme.of(context).textTheme.bodyMedium, label: Text(label), ); @@ -30,6 +35,10 @@ class CommonChip extends StatelessWidget { return ActionChip( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, avatar: avatar, + padding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 4, + ), onPressed: onPressed ?? () {}, side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), labelStyle: Theme.of(context).textTheme.bodyMedium, diff --git a/pubspec.yaml b/pubspec.yaml index ba332e1..9d5231e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.27 +version: 0.8.28 environment: sdk: '>=3.1.0 <4.0.0'