import 'dart:ui' show ImageFilter; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/emby_headers_provider.dart'; import '../../providers/shell_backdrop_provider.dart'; class ShellBackdropLayer extends ConsumerWidget { final bool active; const ShellBackdropLayer({super.key, this.active = false}); static const double _blurSigma = 32; static const Duration _crossfade = Duration(milliseconds: 350); static const LinearGradient _immersiveDarkFallback = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: [0.0, 0.4, 1.0], colors: [Color(0xFF1A1A1A), Color(0xFF0F0F0F), Color(0xFF000000)], ); @override Widget build(BuildContext context, WidgetRef ref) { final backdrop = active ? ref.watch(shellBackdropProvider) : null; final imageHeaders = ref.watch(embyImageHeadersProvider).value; final scheme = Theme.of(context).colorScheme; final Widget child; if (backdrop != null) { child = _BackdropImage( key: ValueKey(backdrop.primaryImageUrl), state: backdrop, imageHeaders: imageHeaders, ); } else if (active) { child = const DecoratedBox( key: ValueKey('__immersive_dark_fallback__'), decoration: BoxDecoration(gradient: _immersiveDarkFallback), ); } else { child = ColoredBox( key: ValueKey('__surface__${scheme.brightness.name}'), color: scheme.surface, ); } return AnimatedSwitcher( duration: _crossfade, switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, layoutBuilder: (currentChild, previousChildren) { final seen = {if (currentChild != null) currentChild.key}; final filtered = previousChildren.where((child) { if (seen.contains(child.key)) return false; seen.add(child.key); return true; }).toList(); return Stack( fit: StackFit.expand, children: [...filtered, if (currentChild != null) currentChild], ); }, child: child, ); } } class _BackdropImage extends StatelessWidget { final ShellBackdropState state; final Map? imageHeaders; const _BackdropImage({ super.key, required this.state, required this.imageHeaders, }); @override Widget build(BuildContext context) { final fallbackUrl = state.fallbackImageUrls.isNotEmpty ? state.fallbackImageUrls.first : state.primaryImageUrl; final surface = Theme.of(context).colorScheme.surface; return Stack( fit: StackFit.expand, children: [ ImageFiltered( imageFilter: ImageFilter.blur( sigmaX: ShellBackdropLayer._blurSigma, sigmaY: ShellBackdropLayer._blurSigma, tileMode: TileMode.clamp, ), child: CachedNetworkImage( imageUrl: state.primaryImageUrl, httpHeaders: imageHeaders, fit: BoxFit.cover, memCacheWidth: 800, fadeInDuration: const Duration(milliseconds: 200), placeholder: (_, _) => ColoredBox(color: surface), errorWidget: (_, _, _) => CachedNetworkImage( imageUrl: fallbackUrl, httpHeaders: imageHeaders, fit: BoxFit.cover, memCacheWidth: 800, placeholder: (_, _) => ColoredBox(color: surface), errorWidget: (_, _, _) => ColoredBox(color: surface), ), ), ), const DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: [0.0, 0.4, 1.0], colors: [Color(0x33000000), Color(0x59000000), Color(0xB3000000)], ), ), ), ], ); } }