Files
sm-emby-share/lib/features/detail/widgets/hero_action_buttons.dart
T

349 lines
9.0 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import '../../../shared/widgets/app_loading_ring.dart';
import '../../../shared/widgets/auto_dismiss_menu.dart';
class HeroIconActionButton extends StatelessWidget {
final IconData icon;
final double height;
final VoidCallback onPressed;
final String? label;
const HeroIconActionButton({
super.key,
required this.icon,
required this.height,
required this.onPressed,
this.label,
});
@override
Widget build(BuildContext context) {
if (label != null) {
return GestureDetector(
onTap: onPressed,
behavior: HitTestBehavior.opaque,
child: SizedBox(
height: height,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 24, color: Colors.white),
const SizedBox(height: 4),
Text(
label!,
style: TextStyle(
fontSize: 11,
color: Colors.white.withValues(alpha: 0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
return SizedBox(
height: height,
child: OutlinedButton(
onPressed: onPressed,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: BorderSide(color: Colors.white.withValues(alpha: 0.4)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.zero,
),
child: Icon(icon, size: height * 0.44),
),
);
}
}
final _heroPillBorder = StadiumBorder(
side: BorderSide(color: Colors.white.withValues(alpha: 0.55)),
);
const _heroDropdownMenuMaxHeight = 320.0;
class HeroPillDropdownButton extends StatelessWidget {
final IconData icon;
final List<FItem> menuChildren;
final double height;
const HeroPillDropdownButton({
super.key,
required this.icon,
required this.menuChildren,
this.height = 52,
});
@override
Widget build(BuildContext context) {
return FPopoverMenu(
menuBuilder: autoDismissMenuBuilder,
menuAnchor: Alignment.topCenter,
childAnchor: Alignment.bottomCenter,
maxHeight: _heroDropdownMenuMaxHeight,
menu: [FItemGroup(children: menuChildren)],
builder: (context, controller, child) => GestureDetector(
onTap: controller.toggle,
child: child,
),
child: Material(
color: Colors.transparent,
shape: _heroPillBorder,
clipBehavior: Clip.antiAlias,
child: SizedBox(
height: height,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Icon(icon, size: 22, color: Colors.white),
),
),
),
);
}
}
class DetailHeroActions extends StatelessWidget {
final bool showReplay;
final VoidCallback? onReplay;
final bool isPlayed;
final VoidCallback onTogglePlayed;
final bool isFavorite;
final VoidCallback onToggleFavorite;
final List<Widget> extraIconActions;
final VoidCallback? onPlay;
final String playLabel;
final double? progressPercent;
final String? trailingLabel;
final bool isPlayLoading;
final bool compact;
const DetailHeroActions({
super.key,
this.showReplay = false,
this.onReplay,
required this.isPlayed,
required this.onTogglePlayed,
required this.isFavorite,
required this.onToggleFavorite,
this.extraIconActions = const [],
this.onPlay,
this.playLabel = '播放',
this.progressPercent,
this.trailingLabel,
this.isPlayLoading = false,
required this.compact,
});
@override
Widget build(BuildContext context) {
final btnH = compact ? 44.0 : 52.0;
final iconRow = Row(
children: [
if (showReplay) ...[
Expanded(
child: HeroIconActionButton(
icon: Icons.replay,
height: btnH,
label: compact ? '重播' : null,
onPressed: onReplay ?? () {},
),
),
SizedBox(width: compact ? 0 : 10),
],
Expanded(
child: HeroIconActionButton(
icon: isPlayed ? Icons.check_circle : Icons.check_circle_outline,
height: btnH,
label: compact ? '已看' : null,
onPressed: onTogglePlayed,
),
),
SizedBox(width: compact ? 0 : 10),
Expanded(
child: HeroIconActionButton(
icon: isFavorite ? Icons.favorite : Icons.favorite_border,
height: btnH,
label: compact ? '收藏' : null,
onPressed: onToggleFavorite,
),
),
for (final extra in extraIconActions) ...[
SizedBox(width: compact ? 0 : 10),
Expanded(child: extra),
],
],
);
final playButton = onPlay != null
? HeroPlayButton(
height: btnH,
label: playLabel,
progressPercent: progressPercent,
trailingLabel: trailingLabel,
isLoading: isPlayLoading,
compact: compact,
onPressed: onPlay,
)
: null;
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: compact ? double.infinity : 360),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: compact
? [
if (playButton != null) ...[
playButton,
const SizedBox(height: 16),
],
iconRow,
]
: [
iconRow,
if (playButton != null) ...[
const SizedBox(height: 12),
playButton,
],
],
),
);
}
}
class HeroPlayButton extends StatelessWidget {
final double height;
final String label;
final IconData icon;
final double? progressPercent;
final String? trailingLabel;
final bool isLoading;
final String loadingLabel;
final bool compact;
final VoidCallback? onPressed;
const HeroPlayButton({
super.key,
required this.height,
required this.label,
this.icon = Icons.play_arrow,
this.progressPercent,
this.trailingLabel,
this.isLoading = false,
this.loadingLabel = '启动中',
this.compact = false,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final radius = BorderRadius.circular(12);
final hasProgress = progressPercent != null && progressPercent! > 0;
final buttonChild = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
isLoading
? const AppLoadingRing(size: 18, color: Colors.black54)
: Icon(icon, size: compact ? 22 : 26),
const SizedBox(width: 6),
Text(
isLoading ? loadingLabel : label,
style: TextStyle(
fontSize: compact ? 15 : 16,
fontWeight: FontWeight.w600,
),
),
if (trailingLabel != null && !isLoading) ...[
const SizedBox(width: 10),
Text(
trailingLabel!,
style: TextStyle(
fontSize: compact ? 13 : 14,
color: Colors.black.withValues(alpha: 0.5),
),
),
],
],
);
if (hasProgress) {
final fraction = (progressPercent! / 100).clamp(0.0, 1.0);
return ClipRRect(
borderRadius: radius,
child: SizedBox(
height: height,
child: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: const [Color(0xFFE3E3E3), Colors.white],
stops: [fraction, fraction],
),
),
),
),
Positioned.fill(
child: FilledButton(
onPressed: isLoading ? null : onPressed,
style: FilledButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Colors.black,
overlayColor: Colors.black.withValues(alpha: 0.08),
shadowColor: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: radius),
),
child: buttonChild,
),
),
],
),
),
);
}
return SizedBox(
height: height,
child: FilledButton(
onPressed: isLoading ? null : onPressed,
style: FilledButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(borderRadius: radius),
),
child: buttonChild,
),
);
}
}