Files
xtremflow/lib/core/widgets/components/ui_components.dart
T
GitHub Copilot 863e5ddad4 refactor(theme): update card and dialog themes to use CardThemeData and DialogThemeData
refactor(ui_components): adjust animation durations for smoother transitions
fix(dashboard): rename border property to hasBorder for clarity
fix(player): rename border property to hasBorder for consistency
2026-03-26 11:20:31 +01:00

501 lines
16 KiB
Dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../theme/app_colors.dart';
import '../../theme/app_theme.dart';
/// Glassmorphism card widget with blur effect
class GlassCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
final double borderRadius;
final VoidCallback? onTap;
final bool showBorder;
const GlassCard({
super.key,
required this.child,
this.padding = const EdgeInsets.all(16),
this.borderRadius = 12,
this.onTap,
this.showBorder = true,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Material(
color: AppColors.glassBackground,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(borderRadius),
child: Container(
padding: padding,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
border: showBorder
? Border.all(color: AppColors.glassBorder, width: 1)
: null,
),
child: child,
),
),
),
),
);
}
}
/// Animated gradient button
class GradientButton extends StatefulWidget {
final String label;
final VoidCallback? onPressed;
final IconData? icon;
final bool isLoading;
final bool isExpanded;
const GradientButton({
super.key,
required this.label,
this.onPressed,
this.icon,
this.isLoading = false,
this.isExpanded = false,
});
@override
State<GradientButton> createState() => _GradientButtonState();
}
class _GradientButtonState extends State<GradientButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: AppTheme.durationSm,
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final button = AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: child,
);
},
child: Container(
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
boxShadow: [
BoxShadow(
color: AppColors.primary.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.isLoading ? null : widget.onPressed,
onTapDown: (_) => _controller.forward(),
onTapUp: (_) => _controller.reverse(),
onTapCancel: () => _controller.reverse(),
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 14,
),
child: widget.isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.black,
),
)
: Row(
mainAxisSize:
widget.isExpanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.icon != null) ...[
Icon(widget.icon, color: Colors.black, size: 20),
const SizedBox(width: 8),
],
Text(
widget.label,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
],
),
),
),
),
),
);
return widget.isExpanded
? SizedBox(width: double.infinity, child: button)
: button;
}
}
/// Shimmer loading effect widget
class ShimmerLoading extends StatefulWidget {
final double width;
final double height;
final double borderRadius;
const ShimmerLoading({
super.key,
this.width = double.infinity,
required this.height,
this.borderRadius = 8,
});
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = Tween<double>(begin: -2, end: 2).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.borderRadius),
gradient: LinearGradient(
begin: Alignment(_animation.value - 1, 0),
end: Alignment(_animation.value + 1, 0),
colors: [
AppColors.surface.withOpacity(0.5),
AppColors.surface,
AppColors.surface.withOpacity(0.5),
],
),
),
);
},
);
}
}
/// Live indicator badge
class LiveBadge extends StatelessWidget {
const LiveBadge({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.live,
borderRadius: BorderRadius.circular(4),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, size: 6, color: Colors.white),
SizedBox(width: 4),
Text(
'LIVE',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
/// Category chip with color
class CategoryChip extends StatelessWidget {
final String label;
final bool isSelected;
final VoidCallback? onTap;
const CategoryChip({
super.key,
required this.label,
this.isSelected = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: isSelected
? AppColors.primary.withOpacity(0.2)
: AppColors.surface,
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
border: Border.all(
color: isSelected ? AppColors.primary : AppColors.border,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? AppColors.primary : AppColors.textSecondary,
),
),
),
),
);
}
}
/// Apple TV style Media Card (Vertical Poster)
class MediaCard extends StatefulWidget {
final String title;
final String? imageUrl;
final String? subtitle;
final String? rating;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final bool isWatched;
final IconData placeholderIcon;
const MediaCard({
super.key,
required this.title,
this.imageUrl,
this.subtitle,
this.rating,
this.onTap,
this.onLongPress,
this.isWatched = false,
this.placeholderIcon = Icons.movie,
});
@override
State<MediaCard> createState() => _MediaCardState();
}
class _MediaCardState extends State<MediaCard>
with SingleTickerProviderStateMixin {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: widget.onTap,
onLongPress: widget.onLongPress,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Poster Image
Expanded(
child: AnimatedScale(
scale: _isHovered ? 1.05 : 1.0,
duration: AppTheme.durationSm,
curve: AppTheme.curveDefault,
child: AnimatedContainer(
duration: AppTheme.durationSm,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
color: AppColors.surface,
border: Border.all(
color: _isHovered ? AppColors.focusColor : Colors.transparent,
width: _isHovered ? 3 : 0,
),
boxShadow: _isHovered
? [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 20,
offset: const Offset(0, 10),
),
]
: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(AppTheme.radiusMd - (_isHovered ? 2 : 0)),
child: Stack(
fit: StackFit.expand,
children: [
if (widget.imageUrl != null)
CachedNetworkImage(
imageUrl: widget.imageUrl!,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: AppColors.surface,
child: Center(
child: Icon(widget.placeholderIcon, color: AppColors.textTertiary),
),
),
errorWidget: (context, url, error) => Container(
color: AppColors.surface,
child: Center(
child: Icon(widget.placeholderIcon, color: AppColors.textTertiary),
),
),
)
else
Container(
color: AppColors.surface,
child: Center(
child: Icon(widget.placeholderIcon, color: AppColors.textTertiary),
),
),
// Rating Badge (Top Left)
if (widget.rating != null && widget.rating!.isNotEmpty)
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, size: 10, color: Color(0xFFFFD700)),
const SizedBox(width: 4),
Text(
widget.rating!,
style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
),
],
),
),
),
// Watched Indicator (Top Right)
if (widget.isWatched)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: AppColors.success,
shape: BoxShape.circle,
),
child: const Icon(Icons.check, size: 12, color: Colors.white),
),
),
],
),
),
),
),
),
// Title & Subtitle (Below)
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: Column(
children: [
Text(
widget.title,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: _isHovered ? AppColors.focusColor : AppColors.textSecondary,
fontWeight: _isHovered ? FontWeight.w600 : FontWeight.w500,
fontSize: 13,
),
),
if (widget.subtitle != null && _isHovered) ...[
const SizedBox(height: 2),
Text(
widget.subtitle!,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: 11,
),
),
],
],
),
),
],
),
),
);
}
}