Fix tray action issues Fix get profile redirect client ua issues Fix proxy card delay view issues Add Russian, Japanese adaptation Fix some issues
432 lines
11 KiB
Dart
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),
|
|
),
|
|
);
|
|
}
|
|
}
|