Initial commit
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user