Files
MWClash/lib/views/proxies/tab.dart
chen08209 9428d4ef4c Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies

Optimize more details
2025-09-20 06:37:29 +08:00

413 lines
12 KiB
Dart

import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/models/config.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';
typedef ProxyGroupViewKeyMap =
Map<String, GlobalObjectKey<_ProxyGroupViewState>>;
class ProxiesTabView extends ConsumerStatefulWidget {
const ProxiesTabView({super.key});
static Map<String, PageStorageKey> pageListStoreMap = {};
@override
ConsumerState<ProxiesTabView> createState() => ProxiesTabViewState();
}
class ProxiesTabViewState extends ConsumerState<ProxiesTabView>
with TickerProviderStateMixin {
TabController? _tabController;
final _hasMoreButtonNotifier = ValueNotifier<bool>(false);
ProxyGroupViewKeyMap _keyMap = {};
@override
void initState() {
super.initState();
ref.listenManual(proxiesTabControllerStateProvider, (prev, next) {
if (prev == next) {
return;
}
if (!stringListEquality.equals(prev?.a, next.a)) {
_destroyTabController();
final index = next.a.indexWhere((item) => item == next.b);
_updateTabController(next.a.length, index);
}
}, fireImmediately: true);
}
@override
void dispose() {
_destroyTabController();
super.dispose();
}
void scrollToGroupSelected() {
final currentGroupName = globalState.appController.getCurrentGroupName();
_keyMap[currentGroupName]?.currentState?.scrollToSelected();
}
Future<void> delayTestCurrentGroup() async {
final currentGroupName = globalState.appController.getCurrentGroupName();
final currentState = _keyMap[currentGroupName]?.currentState;
await delayTest(currentState?.currentProxies ?? [], currentState?.testUrl);
}
Widget _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),
);
},
);
}
void _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(proxiesTabControllerStateProvider);
final groupNames = state.a;
final currentGroupName = state.b;
return SizedBox(
width: double.infinity,
child: Wrap(
alignment: WrapAlignment.center,
runSpacing: 8,
spacing: 8,
children: [
for (final groupName in groupNames)
SettingTextCard(
groupName,
onPressed: () {
final index = groupNames.indexWhere(
(item) => item == groupName,
);
if (index == -1) return;
_tabController?.animateTo(index);
globalState.appController.updateCurrentGroupName(
groupName,
);
Navigator.of(context).pop();
},
isSelected: groupName == currentGroupName,
),
],
),
);
},
),
),
title: appLocalizations.proxyGroup,
);
},
);
}
void _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];
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateCurrentGroupName(currentGroup.name);
});
}
void _destroyTabController() {
_tabController?.removeListener(_tabControllerListener);
_tabController?.dispose();
_tabController = null;
}
void _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);
}
@override
Widget build(BuildContext context) {
ref.watch(themeSettingProvider.select((state) => state.textScale));
final state = ref.watch(proxiesTabStateProvider);
final groups = state.groups;
if (groups.isEmpty) {
return NullStatus(
label: appLocalizations.nullTip(appLocalizations.proxies),
);
}
final ProxyGroupViewKeyMap keyMap = {};
final children = groups.map((group) {
final key = GlobalObjectKey<_ProxyGroupViewState>(group.name);
keyMap[group.name] = key;
return ProxyGroupView(
key: key,
group: group,
columns: state.columns,
cardType: state.proxyCardType,
);
}).toList();
_keyMap = keyMap;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NotificationListener<ScrollMetricsNotification>(
onNotification: (scrollNotification) {
_hasMoreButtonNotifier.value =
scrollNotification.metrics.maxScrollExtent > 0;
return false;
},
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 group in groups) Tab(text: group.name)],
),
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 Group group;
final int columns;
final ProxyCardType cardType;
const ProxyGroupView({
super.key,
required this.group,
required this.columns,
required this.cardType,
});
@override
ConsumerState<ProxyGroupView> createState() => _ProxyGroupViewState();
}
class _ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
late final ScrollController _controller;
List<Proxy> currentProxies = [];
String? testUrl;
@override
void initState() {
super.initState();
_controller = ScrollController();
}
PageStorageKey _getPageStorageKey() {
final profile = globalState.config.currentProfile;
final key =
'${profile?.id}_${ScrollPositionCacheKey.proxiesTabList.name}_${widget.group.name}';
return ProxiesTabView.pageListStoreMap.updateCacheValue(
key,
() => PageStorageKey(key),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void scrollToSelected() {
if (_controller.position.maxScrollExtent == 0) {
return;
}
_controller.animateTo(
min(
16 +
getScrollToSelectedOffset(
groupName: widget.group.name,
proxies: currentProxies,
),
_controller.position.maxScrollExtent,
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
}
@override
Widget build(BuildContext context) {
final group = widget.group;
final proxies = group.all;
testUrl = group.testUrl;
currentProxies = proxies;
return CommonScrollBar(
controller: _controller,
child: GridView.builder(
key: _getPageStorageKey(),
controller: _controller,
padding: const EdgeInsets.only(
top: 16,
left: 16,
right: 16,
bottom: 96,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: getItemHeight(widget.cardType),
),
itemCount: currentProxies.length,
itemBuilder: (_, index) {
final proxy = currentProxies[index];
return ProxyCard(
testUrl: group.testUrl,
groupType: group.type,
type: widget.cardType,
proxy: proxy,
groupName: group.name,
);
},
),
);
}
}
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;
Future<void> _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),
),
);
}
}