Files
FlClash/lib/views/proxies/card.dart
T

359 lines
10 KiB
Dart
Raw Normal View History

2024-07-15 22:06:09 +08:00
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/icons/icons.dart';
2024-07-15 22:06:09 +08:00
import 'package:fl_clash/models/models.dart';
2025-02-09 18:39:38 +08:00
import 'package:fl_clash/providers/providers.dart';
2024-07-15 22:06:09 +08:00
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
2025-02-09 18:39:38 +08:00
import 'package:flutter_riverpod/flutter_riverpod.dart';
2024-07-15 22:06:09 +08:00
void selectGroupProxy(
WidgetRef ref, {
required String groupName,
required GroupType groupType,
required String proxyName,
}) {
final isComputedSelected = groupType.isComputedSelected;
if (isComputedSelected || groupType == GroupType.Selector) {
final currentProxyName = ref.read(proxyNameProvider(groupName));
final nextProxyName = switch (isComputedSelected) {
true => currentProxyName == proxyName ? '' : proxyName,
false => proxyName,
};
ref
.read(proxiesActionProvider.notifier)
.changeProxyDebounce(groupName, nextProxyName);
return;
}
dialogs.showNotifier(
currentAppLocalizations.notSelectedTip,
level: MessageLevel.warning,
);
}
class ProxyCard extends ConsumerWidget {
2024-07-15 22:06:09 +08:00
final String groupName;
final Proxy proxy;
2024-07-26 08:05:22 +08:00
final GroupType groupType;
2024-07-15 22:06:09 +08:00
final ProxyCardType type;
2025-01-13 19:08:17 +08:00
final String? testUrl;
2024-07-15 22:06:09 +08:00
const ProxyCard({
super.key,
required this.groupName,
2025-01-13 19:08:17 +08:00
required this.testUrl,
2024-07-15 22:06:09 +08:00
required this.proxy,
2024-07-26 08:05:22 +08:00
required this.groupType,
2024-07-15 22:06:09 +08:00
required this.type,
});
2024-09-05 08:59:45 +08:00
Measure get measure => globalState.measure;
2024-07-15 22:06:09 +08:00
Widget _buildProxyNameText(BuildContext context) {
if (type == ProxyCardType.min) {
return SizedBox(
height: measure.bodyMediumHeight * 1,
2024-08-13 21:42:23 +08:00
child: EmojiText(
2024-07-15 22:06:09 +08:00
proxy.name,
maxLines: 1,
2024-08-13 21:42:23 +08:00
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium,
2024-07-15 22:06:09 +08:00
),
);
} else {
return SizedBox(
height: measure.bodyMediumHeight * 2,
2024-08-13 21:42:23 +08:00
child: EmojiText(
2024-07-15 22:06:09 +08:00
proxy.name,
maxLines: 2,
2024-08-13 21:42:23 +08:00
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium,
2024-07-15 22:06:09 +08:00
),
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
2024-09-05 08:59:45 +08:00
final measure = globalState.measure;
final delayText = _DelayText(proxy: proxy, testUrl: testUrl, type: type);
2024-07-15 22:06:09 +08:00
final proxyNameText = _buildProxyNameText(context);
2025-02-09 18:39:38 +08:00
return Stack(
children: [
Consumer(
builder: (_, ref, child) {
2025-07-31 17:09:18 +08:00
final selectedProxyName = ref.watch(
2026-02-02 10:14:13 +08:00
selectedProxyNameProvider(groupName),
2025-07-31 17:09:18 +08:00
);
2025-02-09 18:39:38 +08:00
return CommonCard(
radius: AppCorner.lg,
enterActionsOnRight: true,
2024-07-26 08:05:22 +08:00
key: key,
onPressed: () {
selectGroupProxy(
ref,
groupName: groupName,
groupType: groupType,
proxyName: proxy.name,
);
2024-07-26 08:05:22 +08:00
},
2025-02-09 18:39:38 +08:00
isSelected: selectedProxyName == proxy.name,
child: child!,
);
},
child: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
proxyNameText,
2025-07-31 17:09:18 +08:00
const SizedBox(height: 8),
2025-02-09 18:39:38 +08:00
if (type == ProxyCardType.expand) ...[
SizedBox(
height: measure.bodySmallHeight,
2025-07-31 17:09:18 +08:00
child: _ProxyDesc(proxy: proxy),
2025-02-09 18:39:38 +08:00
),
2025-07-31 17:09:18 +08:00
const SizedBox(height: 6),
2025-02-09 18:39:38 +08:00
delayText,
] else
SizedBox(
height: measure.bodySmallHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
proxy.type,
maxLines: 1,
2024-08-13 21:42:23 +08:00
style: context.textTheme.bodySmall?.copyWith(
2025-02-09 18:39:38 +08:00
overflow: TextOverflow.ellipsis,
2025-05-02 02:24:12 +08:00
color: context
2025-07-31 17:09:18 +08:00
.textTheme
.bodySmall
?.color
?.opacity80,
2024-07-26 08:05:22 +08:00
),
),
2025-02-09 18:39:38 +08:00
),
2024-07-15 22:06:09 +08:00
),
2025-02-09 18:39:38 +08:00
delayText,
],
2024-07-26 08:05:22 +08:00
),
),
2025-02-09 18:39:38 +08:00
],
),
),
),
if (groupType.isComputedSelected)
Positioned(
top: 0,
right: 0,
2025-07-31 17:09:18 +08:00
child: _ProxyComputedMark(groupName: groupName, proxy: proxy),
),
2025-02-09 18:39:38 +08:00
],
);
}
}
class _DelayText extends ConsumerStatefulWidget {
const _DelayText({
required this.proxy,
required this.testUrl,
required this.type,
});
final Proxy proxy;
final String? testUrl;
final ProxyCardType type;
@override
ConsumerState<_DelayText> createState() => _DelayTextState();
}
class _DelayTextState extends ConsumerState<_DelayText> {
final FocusNode _focusNode = SkipTraversalFocusNode();
bool _isFocused = false;
@override
void initState() {
super.initState();
_focusNode.addListener(_handleFocusChange);
}
@override
void dispose() {
_focusNode.removeListener(_handleFocusChange);
_focusNode.dispose();
super.dispose();
}
void _handleFocusChange() {
final isFocused = _focusNode.hasPrimaryFocus;
if (isFocused == _isFocused) {
return;
}
setState(() {
_isFocused = isFocused;
});
}
void _handleTestCurrentDelay() {
ref
.read(proxiesActionProvider.notifier)
.proxyDelayTest(widget.proxy, widget.testUrl);
}
Widget _withFocusRing(BuildContext context, Widget child) {
if (!_isFocused) {
return child;
}
return Stack(
clipBehavior: Clip.none,
children: [
child,
Positioned(
top: -2,
right: -3,
bottom: -2,
left: -3,
child: IgnorePointer(
child: DecoratedBox(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: AppRadius.xs,
side: BorderSide(
color: context.colorScheme.primary,
width: 2,
),
),
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final measure = globalState.measure;
final delay = ref.watch(
delayProvider(proxyName: widget.proxy.name, testUrl: widget.testUrl),
);
final phase = ref.watch(
delayTestPhaseProvider(
proxyName: widget.proxy.name,
testUrl: widget.testUrl,
),
);
return Actions(
actions: {
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (_) {
_handleTestCurrentDelay();
return null;
},
),
},
child: Focus(
focusNode: _focusNode,
child: SizedBox(
height: measure.labelSmallHeight,
child: FadeBox(
alignment: widget.type == ProxyCardType.expand
? Alignment.centerLeft
: Alignment.centerRight,
child: phase != null || delay == null
? SizedBox(
height: measure.labelSmallHeight,
width: measure.labelSmallHeight,
child: _withFocusRing(context, switch (phase) {
DelayTestPhase.running => const CommonCircleLoading(),
DelayTestPhase.queued => GlyphIcon(
AppGlyphs.clock,
size: measure.labelSmallHeight,
color: context.colorScheme.onSurfaceVariant.opacity38,
),
null => IconButton(
tooltip: context.appLocalizations.delayTest,
icon: const GlyphIcon(AppGlyphs.bolt),
iconSize: measure.labelSmallHeight,
padding: EdgeInsets.zero,
onPressed: _handleTestCurrentDelay,
),
}),
)
: GestureDetector(
onTap: _handleTestCurrentDelay,
child: _withFocusRing(
context,
Text(
delay > 0
? '$delay ms'
: delayFailureText(delay, context.appLocalizations),
maxLines: 1,
style: context.textTheme.labelSmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context.colorScheme.delayColor(delay),
),
),
),
),
),
),
),
);
}
}
2025-02-09 18:39:38 +08:00
class _ProxyDesc extends ConsumerWidget {
final Proxy proxy;
2025-07-31 17:09:18 +08:00
const _ProxyDesc({required this.proxy});
2025-02-09 18:39:38 +08:00
@override
Widget build(BuildContext context, WidgetRef ref) {
2026-02-02 10:14:13 +08:00
final desc = ref.watch(proxyDescProvider(proxy));
2025-02-09 18:39:38 +08:00
return EmojiText(
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
2025-03-12 17:15:31 +08:00
color: context.textTheme.bodySmall?.color?.opacity80,
2025-02-09 18:39:38 +08:00
),
);
}
}
class _ProxyComputedMark extends ConsumerWidget {
final String groupName;
final Proxy proxy;
2025-07-31 17:09:18 +08:00
const _ProxyComputedMark({required this.groupName, required this.proxy});
2025-02-09 18:39:38 +08:00
@override
Widget build(BuildContext context, WidgetRef ref) {
2026-02-02 10:14:13 +08:00
final proxyName = ref.watch(proxyNameProvider(groupName));
2025-02-09 18:39:38 +08:00
if (proxyName != proxy.name) {
2026-02-02 10:14:13 +08:00
return const SizedBox();
2025-02-09 18:39:38 +08:00
}
return 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(),
),
2024-07-15 22:06:09 +08:00
);
}
}