Files
xianyan/lib/features/discover/presentation/widgets/tool/tool_grid_item.dart
Developer bd083976c6 补充
2026-06-01 08:17:08 +08:00

232 lines
8.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 工具网格项组件
/// 创建时间: 2026-04-26
/// 更新时间: 2026-05-30
/// 作用: 工具中心4列网格中的单个工具图标 + 长按菜单
/// 上次更新: 添加Semantics无障碍支持 + 版本角标 + 状态指示
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/l10n/translations.dart';
import 'package:xianyan/core/theme/app_theme.dart';
import 'package:xianyan/core/theme/app_typography.dart';
import 'package:xianyan/core/theme/app_radius.dart';
import 'package:xianyan/shared/widgets/input/app_popup_menu.dart';
import '../../../models/tool_item.dart';
class ToolGridItem extends ConsumerStatefulWidget {
const ToolGridItem({
super.key,
required this.tool,
this.onTap,
this.onLongPress,
this.onFavorite,
this.onPin,
this.onShare,
this.showBadge = false,
this.size = 48.0,
});
final ToolItem tool;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final VoidCallback? onFavorite;
final VoidCallback? onPin;
final VoidCallback? onShare;
final bool showBadge;
final double size;
@override
ConsumerState<ToolGridItem> createState() => _ToolGridItemState();
}
class _ToolGridItemState extends ConsumerState<ToolGridItem>
with SingleTickerProviderStateMixin {
late final AnimationController _scaleController;
late final Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_scaleController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.92,
).animate(CurvedAnimation(parent: _scaleController, curve: Curves.easeOut));
}
@override
void dispose() {
_scaleController.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails _) => _scaleController.forward();
void _onTapUp(TapUpDetails _) => _scaleController.reverse();
void _onTapCancel() => _scaleController.reverse();
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return AppPopupMenu(
items: [
PopupMenuItemConfig(
type: PopupMenuItemType.favorite,
title: widget.tool.isFavorited ? ref.watch(translationsProvider).discover.cancelFavorite : ref.watch(translationsProvider).discover.favoriteAction,
icon: widget.tool.isFavorited
? CupertinoIcons.heart_fill
: CupertinoIcons.heart,
onSelected: widget.onFavorite,
),
PopupMenuItemConfig(
type: PopupMenuItemType.pin,
title: widget.tool.isPinned ? ref.watch(translationsProvider).discover.cancelPin : ref.watch(translationsProvider).discover.pinToTop,
icon: widget.tool.isPinned
? CupertinoIcons.pin_slash
: CupertinoIcons.pin,
onSelected: widget.onPin,
),
PopupMenuItemConfig(
type: PopupMenuItemType.share,
title: ref.watch(translationsProvider).discover.shareAction,
onSelected: widget.onShare,
),
],
buttonBuilder: (context, showMenu) => Semantics(
label: '${widget.tool.name} ${ref.watch(translationsProvider).discover.toolCenter}',
hint: widget.tool.description ?? '${ref.watch(translationsProvider).discover.tapToOpen}${ref.watch(translationsProvider).discover.longPressForMore}',
button: true,
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
onTap: widget.onTap,
onLongPress: showMenu,
child: ScaleTransition(
scale: _scaleAnimation,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: widget.tool.gradientColors,
),
borderRadius: BorderRadius.circular(widget.size * 0.29),
boxShadow: [
BoxShadow(
color: widget.tool.gradientColors.first.withValues(
alpha: widget.tool.status == ToolStatus.available ? 0.3 : 0.1,
),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Opacity(
opacity: widget.tool.status == ToolStatus.available ? 1.0 : 0.5,
child: Center(
child: Text(
widget.tool.emoji,
style: TextStyle(fontSize: widget.size * 0.42),
),
),
),
),
if (widget.showBadge || widget.tool.isNew)
Positioned(
top: -3,
right: -3,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: CupertinoColors.systemRed,
borderRadius: AppRadius.smBorder,
),
child: Text(
ref.watch(translationsProvider).discover.newBadge,
style: const TextStyle(
fontSize: 8,
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
),
),
),
),
if (widget.tool.isPinned)
Positioned(
bottom: -2,
right: -2,
child: Container(
width: 14,
height: 14,
decoration: const BoxDecoration(
color: CupertinoColors.activeOrange,
shape: BoxShape.circle,
),
child: const Icon(
CupertinoIcons.pin_fill,
size: 8,
color: CupertinoColors.white,
),
),
),
if (widget.tool.status == ToolStatus.offline)
Positioned(
bottom: -2,
left: -2,
child: Container(
width: 10,
height: 10,
decoration: const BoxDecoration(
color: CupertinoColors.systemGrey,
shape: BoxShape.circle,
),
child: const Icon(
CupertinoIcons.wifi_slash,
size: 6,
color: CupertinoColors.white,
),
),
),
],
),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
widget.tool.name,
style: AppTypography.caption2.copyWith(
color: widget.tool.status == ToolStatus.available
? ext.textPrimary
: ext.textHint,
),
textAlign: TextAlign.center,
maxLines: 1,
),
),
],
),
),
),
),
);
}
}