Files
MWClash/lib/fragments/proxies/tab.dart
chen08209 e92900dbbd Fix tab delay view issues
Fix tray action issues

Fix get profile redirect client ua issues

Fix proxy card delay view issues

Add Russian, Japanese adaptation

Fix some issues
2025-03-07 23:49:27 +08:00

432 lines
11 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(viewWidthProvider.notifier).isMobileView;
return IconButton(
onPressed: _showMoreMenu,
icon: isMobileView
? const Icon(
Icons.expand_more,
)
: const Icon(
Icons.chevron_right,
),
);
},
);
}
_showMoreMenu() {
showSheet(
context: context,
width: 380,
isScrollControlled: false,
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.withOpacity(0.1),
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: 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),
),
);
}
}