Files
MWClash/lib/fragments/proxies.dart
chen08209 7ea226ef4a Add one-click update all profiles
Add expire show
2024-06-08 15:04:18 +08:00

488 lines
14 KiB
Dart

import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../enum/enum.dart';
import '../models/models.dart';
import '../common/common.dart';
import '../widgets/widgets.dart';
class ProxiesFragment extends StatefulWidget {
const ProxiesFragment({super.key});
@override
State<ProxiesFragment> createState() => _ProxiesFragmentState();
}
class _ProxiesFragmentState extends State<ProxiesFragment>
with TickerProviderStateMixin {
TabController? _tabController;
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
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<Config, ProxiesSortType>(
selector: (_, config) => config.proxiesSortType,
builder: (_, proxiesSortType, __) {
return CommonPopupMenu<ProxiesSortType>.radio(
items: items,
onSelected: (value) {
final config = context.read<Config>();
config.proxiesSortType = value;
},
selectedValue: proxiesSortType,
);
},
),
const SizedBox(
width: 8,
)
];
});
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector3<AppState, Config, ClashConfig, ProxiesSelectorState>(
selector: (_, appState, config, clashConfig) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
);
},
shouldRebuild: (prev, next) {
if (prev.groupNames.length != next.groupNames.length) {
_tabController?.dispose();
_tabController = null;
}
return prev != next;
},
builder: (_, state, __) {
_tabController ??= TabController(
length: state.groupNames.length,
vsync: this,
);
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<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
(a, b) => other.sortByChar(a.name, b.name),
);
}
List<Proxy> _sortOfDelay(BuildContext context, List<Proxy> proxies) {
final appState = context.read<AppState>();
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<Proxy> 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<Proxy> proxies) async {
for (final proxy in proxies) {
globalState.appController.setDelay(
Delay(name: proxy.name, value: 0),
);
clashCore.getDelay(proxy.name).then((delay) {
globalState.appController.setDelay(delay);
});
}
await Future.delayed(httpTimeoutDuration + moreDuration);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesTabViewSelectorState>(
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<AppState, Config, ClashConfig, ProxiesCardSelectorState>(
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<AppState, String>(
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<AppState, int?>(
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<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
_healthcheck() async {
_controller.forward();
await widget.onClick();
_controller.reverse();
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 600,
),
);
_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) {
_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,
);
}
}