Files
MWClash/lib/fragments/proxies/tab.dart
chen08209 a8c0519ffe Add rule override
Optimize more details
2025-04-06 20:13:53 +08:00

441 lines
12 KiB
Dart

import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'card.dart';
import 'common.dart';
List<Proxy> currentTabProxies = [];
String? currentTabTestUrl;
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
class ProxiesTabFragment extends ConsumerStatefulWidget {
const ProxiesTabFragment({super.key});
@override
ConsumerState<ProxiesTabFragment> createState() => ProxiesTabFragmentState();
}
class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
with TickerProviderStateMixin {
TabController? _tabController;
final _hasMoreButtonNotifier = ValueNotifier<bool>(false);
GroupNameKeyMap _keyMap = {};
@override
void initState() {
super.initState();
_handleTabListen();
}
@override
void dispose() {
_destroyTabController();
super.dispose();
}
scrollToGroupSelected() {
final currentGroupName = globalState.appController.getCurrentGroupName();
_keyMap[currentGroupName]?.currentState?.scrollToSelected();
}
_buildMoreButton() {
return Consumer(
builder: (_, ref, ___) {
final isMobileView = ref.watch(isMobileViewProvider);
return IconButton(
onPressed: _showMoreMenu,
icon: isMobileView
? const Icon(
Icons.expand_more,
)
: const Icon(
Icons.chevron_right,
),
);
},
);
}
_showMoreMenu() {
showSheet(
context: context,
props: SheetProps(
isScrollControlled: false,
),
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Consumer(
builder: (_, ref, __) {
final state = ref.watch(proxiesSelectorStateProvider);
return SizedBox(
width: double.infinity,
child: Wrap(
alignment: WrapAlignment.center,
runSpacing: 8,
spacing: 8,
children: [
for (final groupName in state.groupNames)
SettingTextCard(
groupName,
onPressed: () {
final index = state.groupNames.indexWhere(
(item) => item == groupName,
);
if (index == -1) return;
_tabController?.animateTo(index);
globalState.appController
.updateCurrentGroupName(groupName);
Navigator.of(context).pop();
},
isSelected: groupName == state.currentGroupName,
)
],
),
);
},
),
),
title: appLocalizations.proxyGroup,
);
},
);
}
_tabControllerListener([int? index]) {
int? groupIndex = index;
if (groupIndex == -1) {
return;
}
final appController = globalState.appController;
if (groupIndex == null) {
final currentIndex = _tabController?.index;
groupIndex = currentIndex;
}
final currentGroups = appController.getCurrentGroups();
if (groupIndex == null || groupIndex > currentGroups.length) {
return;
}
final currentGroup = currentGroups[groupIndex];
currentTabProxies = currentGroup.all;
currentTabTestUrl = currentGroup.testUrl;
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateCurrentGroupName(
currentGroup.name,
);
});
}
_destroyTabController() {
_tabController?.removeListener(_tabControllerListener);
_tabController?.dispose();
_tabController = null;
}
_updateTabController(int length, int index) {
if (length == 0) {
_destroyTabController();
return;
}
final realIndex = index == -1 ? 0 : index;
_tabController ??= TabController(
length: length,
initialIndex: realIndex,
vsync: this,
);
_tabControllerListener(realIndex);
_tabController?.addListener(_tabControllerListener);
}
_handleTabListen() {
ref.listenManual(
proxiesSelectorStateProvider,
(prev, next) {
if (prev == next) {
return;
}
if (prev?.groupNames.length != next.groupNames.length) {
_destroyTabController();
final index = next.groupNames.indexWhere(
(item) => item == next.currentGroupName,
);
_updateTabController(next.groupNames.length, index);
}
},
fireImmediately: true,
);
}
@override
Widget build(BuildContext context) {
final state = ref.watch(groupNamesStateProvider);
final groupNames = state.groupNames;
if (groupNames.isEmpty) {
return NullStatus(
label: appLocalizations.nullProxies,
);
}
final GroupNameKeyMap keyMap = {};
final children = groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName);
return KeepScope(
child: ProxyGroupView(
key: keyMap[groupName],
groupName: groupName,
),
);
}).toList();
_keyMap = keyMap;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NotificationListener<ScrollMetricsNotification>(
onNotification: (scrollNotification) {
_hasMoreButtonNotifier.value =
scrollNotification.metrics.maxScrollExtent > 0;
return true;
},
child: ValueListenableBuilder(
valueListenable: _hasMoreButtonNotifier,
builder: (_, value, child) {
return Stack(
alignment: AlignmentDirectional.centerStart,
children: [
TabBar(
controller: _tabController,
padding: EdgeInsets.only(
left: 16,
right: 16 + (value ? 16 : 0),
),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor:
const WidgetStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in groupNames)
Tab(
text: groupName,
),
],
),
if (value)
Positioned(
right: 0,
child: child!,
),
],
);
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
context.colorScheme.surface.opacity10,
context.colorScheme.surface,
],
stops: const [
0.0,
0.1
]),
),
child: _buildMoreButton(),
),
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: children,
),
)
],
);
}
}
class ProxyGroupView extends ConsumerStatefulWidget {
final String groupName;
const ProxyGroupView({
super.key,
required this.groupName,
});
@override
ConsumerState<ProxyGroupView> createState() => ProxyGroupViewState();
}
class ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
final _controller = ScrollController();
String get groupName => widget.groupName;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
scrollToSelected() {
if (_controller.position.maxScrollExtent == 0) {
return;
}
final sortedProxies = globalState.appController.getSortProxies(
currentTabProxies,
currentTabTestUrl,
);
_controller.animateTo(
min(
16 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: sortedProxies,
),
_controller.position.maxScrollExtent,
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
}
@override
Widget build(BuildContext context) {
final state = ref.watch(proxyGroupSelectorStateProvider(groupName));
final proxies = state.proxies;
final columns = state.columns;
final proxyCardType = state.proxyCardType;
final sortedProxies = globalState.appController.getSortProxies(
proxies,
state.testUrl,
);
return Align(
alignment: Alignment.topCenter,
child: CommonAutoHiddenScrollBar(
controller: _controller,
child: GridView.builder(
controller: _controller,
padding: const EdgeInsets.only(
top: 16,
left: 16,
right: 16,
bottom: 96,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return ProxyCard(
testUrl: state.testUrl,
groupType: state.groupType,
type: proxyCardType,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
),
),
);
}
}
class DelayTestButton extends StatefulWidget {
final Future Function() onClick;
const DelayTestButton({
super.key,
required this.onClick,
});
@override
State<DelayTestButton> createState() => _DelayTestButtonState();
}
class _DelayTestButtonState extends State<DelayTestButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
_healthcheck() async {
if (_controller.isAnimating) {
return;
}
_controller.forward();
await widget.onClick();
if (mounted) {
_controller.reverse();
}
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 200,
),
);
_scale = Tween<double>(
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) {
return 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),
),
);
}
}