import 'package:flutter/material.dart'; import 'media_row_metrics.dart'; import 'media_poster_card.dart'; class SkeletonMediaRow extends StatefulWidget { final String title; final MediaCardVariant cardVariant; final Color? titleColor; const SkeletonMediaRow({ super.key, required this.title, this.cardVariant = MediaCardVariant.poster, this.titleColor, }); @override State createState() => _SkeletonMediaRowState(); } class _SkeletonMediaRowState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } double get _cardAspect => widget.cardVariant == MediaCardVariant.landscape ? 16 / 9 : 2 / 3; bool get _showSubtitlePlaceholder => widget.cardVariant == MediaCardVariant.landscape; @override Widget build(BuildContext context) { final theme = Theme.of(context); final isLight = theme.brightness == Brightness.light; final compact = MediaRowMetrics.isCompact(context); final rowHeight = MediaRowMetrics.rowHeight( widget.cardVariant, compact: compact, ); final cardWidth = MediaRowMetrics.cardWidth( widget.cardVariant, compact: compact, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 12, 24, 10), child: Text( widget.title, style: theme.textTheme.titleMedium?.copyWith( color: widget.titleColor, ), ), ), SizedBox( height: rowHeight, child: ListView.separated( scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 24), itemCount: 6, separatorBuilder: (_, _) => const SizedBox(width: 14), itemBuilder: (_, _) => _SkeletonCard( width: cardWidth, aspect: _cardAspect, showSubtitle: _showSubtitlePlaceholder, isLight: isLight, animation: _controller, ), ), ), ], ); } } class _SkeletonCard extends StatelessWidget { final double width; final double aspect; final bool showSubtitle; final bool isLight; final Animation animation; const _SkeletonCard({ required this.width, required this.aspect, required this.showSubtitle, required this.isLight, required this.animation, }); @override Widget build(BuildContext context) { return SizedBox( width: width, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: aspect, child: AnimatedBuilder( animation: animation, builder: (context, child) { final base = isLight ? const Color(0xFFF3F4F6) : const Color(0xFF2A2A2A); final highlight = isLight ? const Color(0xFFE5E7EB) : const Color(0xFF3A3A3A); return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), gradient: LinearGradient( begin: Alignment(-1.0 + 2.0 * animation.value, 0), end: Alignment(-1.0 + 2.0 * animation.value + 1.0, 0), colors: [base, highlight, base], stops: const [0.0, 0.5, 1.0], ), ), ); }, ), ), const SizedBox(height: 8), AnimatedBuilder( animation: animation, builder: (context, child) { final base = isLight ? const Color(0xFFF3F4F6) : const Color(0xFF2A2A2A); final highlight = isLight ? const Color(0xFFE5E7EB) : const Color(0xFF3A3A3A); return Container( height: 12, width: width * 0.7, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), gradient: LinearGradient( begin: Alignment(-1.0 + 2.0 * animation.value, 0), end: Alignment(-1.0 + 2.0 * animation.value + 1.0, 0), colors: [base, highlight, base], stops: const [0.0, 0.5, 1.0], ), ), ); }, ), if (showSubtitle) ...[ const SizedBox(height: 6), AnimatedBuilder( animation: animation, builder: (context, child) { final base = isLight ? const Color(0xFFF3F4F6) : const Color(0xFF2A2A2A); final highlight = isLight ? const Color(0xFFE5E7EB) : const Color(0xFF3A3A3A); return Container( height: 10, width: width * 0.5, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), gradient: LinearGradient( begin: Alignment(-1.0 + 2.0 * animation.value, 0), end: Alignment(-1.0 + 2.0 * animation.value + 1.0, 0), colors: [base, highlight, base], stops: const [0.0, 0.5, 1.0], ), ), ); }, ), ], ], ), ); } }