import 'dart:io' show Platform; import 'dart:ui' show ImageFilter; import 'package:flutter/material.dart'; import '../../../shared/widgets/smart_image.dart'; class DetailBackdrop extends StatefulWidget { const DetailBackdrop({ super.key, required this.scrollProgress, required this.backdropUrl, required this.fallbackUrls, required this.embyBaseUrl, required this.imageHeaders, this.gradientEndColor = const Color(0xFF08090c), }); final ValueNotifier scrollProgress; final String backdropUrl; final List fallbackUrls; final String embyBaseUrl; final Map? imageHeaders; final Color gradientEndColor; @override State createState() => _DetailBackdropState(); } class _DetailBackdropState extends State { bool _backdropFailed = false; @override void didUpdateWidget(DetailBackdrop oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.backdropUrl != widget.backdropUrl) { _backdropFailed = false; } } @override Widget build(BuildContext context) { return Stack( children: [ Positioned.fill( child: _backdropFailed ? _buildFallbackGradient() : RepaintBoundary( child: ValueListenableBuilder( valueListenable: widget.scrollProgress, builder: (_, p, child) { final maxSigma = Platform.isAndroid ? 6.0 : 10.0; var sigma = p > 0.01 ? p * maxSigma : 0.0; if (Platform.isAndroid && sigma > 0) { sigma = (sigma / 1.5).roundToDouble() * 1.5; } final blurred = ImageFiltered( imageFilter: ImageFilter.blur( sigmaX: sigma, sigmaY: sigma, ), child: child, ); if (Platform.isAndroid) return blurred; return Transform.scale( scale: 1.0 + p * 0.15, child: blurred, ); }, child: _buildImageStack(), ), ), ), Positioned.fill( child: ValueListenableBuilder( valueListenable: widget.scrollProgress, builder: (_, p, _) { return IgnorePointer( child: ColoredBox( color: Colors.black.withValues(alpha: 0.10 + p * 0.3), ), ); }, ), ), Positioned.fill( child: IgnorePointer(child: _buildBottomGradient(context)), ), ], ); } Widget _buildBottomGradient(BuildContext context) { final size = MediaQuery.sizeOf(context); final coverage = (size.width * 9.0 / 16.0 / size.height).clamp(0.0, 1.0); final rampStart = coverage >= 0.95 ? 0.5 : (coverage * 0.55).clamp(0.1, 0.5); final rampEnd = coverage >= 0.95 ? 0.75 : (coverage * 0.85).clamp(0.25, 0.75); return DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: [0.0, rampStart, rampEnd, 1.0], colors: [ Colors.transparent, Colors.transparent, widget.gradientEndColor.withValues(alpha: 0.85), widget.gradientEndColor, ], ), ), ); } Widget _buildFallbackGradient() { return IgnorePointer( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [0.0, 0.6, 1.0], colors: [ widget.gradientEndColor.withValues(alpha: 0.5), widget.gradientEndColor.withValues(alpha: 0.85), widget.gradientEndColor, ], ), ), ), ); } Widget _buildImageStack() { return _buildArtworkImage( fit: BoxFit.fitWidth, alignment: Alignment.topCenter, ); } Widget _buildArtworkImage({ required BoxFit fit, required Alignment alignment, int? memCacheWidth, }) { final placeholder = DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ widget.gradientEndColor.withValues(alpha: 0.3), widget.gradientEndColor, ], ), ), ); final imageStack = Stack( fit: StackFit.expand, children: [ ColoredBox(color: widget.gradientEndColor), SmartImage( url: widget.backdropUrl, fallbackUrls: widget.fallbackUrls, fit: fit, alignment: alignment, borderRadius: 0, memCacheWidth: memCacheWidth, placeholder: placeholder, onError: () { if (mounted && !_backdropFailed) { setState(() => _backdropFailed = true); } }, resolveHttpHeaders: (url) => widget.embyBaseUrl.isNotEmpty && url.startsWith(widget.embyBaseUrl) ? widget.imageHeaders : null, ), ], ); return imageStack; } }