import 'dart:ui' show ImageFilter; import 'package:flutter/material.dart'; import '../../../shared/widgets/smart_image.dart'; import '../widgets/detail_nav_bar.dart'; import 'android_detail_banner.dart'; import 'android_detail_colors.dart'; import 'android_detail_geometry.dart'; class AndroidDetailLayout extends StatelessWidget { const AndroidDetailLayout({ super.key, required this.scrollController, required this.scrollProgress, this.backdropUrl, this.fallbackUrls = const [], this.embyBaseUrl = '', this.imageHeaders, this.backgroundColor, this.navigationColor, required this.title, required this.onBack, required this.heroInfo, this.heroActions, this.overview, this.belowOverview, this.sections = const [], }); final ScrollController scrollController; final ValueNotifier scrollProgress; final String? backdropUrl; final List fallbackUrls; final String embyBaseUrl; final Map? imageHeaders; final Color? backgroundColor; final Color? navigationColor; final String title; final VoidCallback onBack; final Widget heroInfo; final Widget? heroActions; final Widget? overview; final Widget? belowOverview; final List sections; @override Widget build(BuildContext context) { final topInset = MediaQuery.paddingOf(context).top; final bannerHeight = computeAndroidDetailBannerHeight(context); final backdrop = backdropUrl ?? (fallbackUrls.isNotEmpty ? fallbackUrls.first : null); final effectiveFallbackUrls = backdropUrl != null ? fallbackUrls : fallbackUrls.skip(1).toList(growable: false); final targetBackgroundColor = backgroundColor ?? AndroidDetailColors.background; final animationDuration = MediaQuery.disableAnimationsOf(context) ? Duration.zero : const Duration(milliseconds: 350); return TweenAnimationBuilder( tween: ColorTween( begin: AndroidDetailColors.background, end: targetBackgroundColor, ), duration: animationDuration, curve: Curves.easeOutCubic, builder: (context, animatedBackgroundColor, child) { final effectiveBackgroundColor = animatedBackgroundColor ?? targetBackgroundColor; return Scaffold( backgroundColor: effectiveBackgroundColor, body: Stack( children: [ Positioned.fill( child: _AndroidDetailBlurredBackdrop( imageUrl: backdrop, fallbackUrls: effectiveFallbackUrls, embyBaseUrl: embyBaseUrl, imageHeaders: imageHeaders, ), ), CustomScrollView( controller: scrollController, physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), slivers: [ if (backdrop != null) SliverAppBar( automaticallyImplyLeading: false, primary: false, pinned: false, stretch: true, toolbarHeight: 0, collapsedHeight: 0, expandedHeight: bannerHeight, backgroundColor: Colors.transparent, surfaceTintColor: Colors.transparent, flexibleSpace: FlexibleSpaceBar( collapseMode: CollapseMode.parallax, stretchModes: const [StretchMode.zoomBackground], background: AndroidDetailBanner( scrollProgress: scrollProgress, backdropUrl: backdrop, fallbackUrls: effectiveFallbackUrls, embyBaseUrl: embyBaseUrl, imageHeaders: imageHeaders, ), ), ) else SliverToBoxAdapter(child: SizedBox(height: topInset + 72)), SliverToBoxAdapter(child: heroInfo), if (heroActions != null) SliverToBoxAdapter(child: heroActions!), if (overview != null) SliverToBoxAdapter(child: overview!), if (belowOverview != null) SliverToBoxAdapter(child: belowOverview!), SliverList.list( children: [ ...sections, SizedBox( height: 32 + MediaQuery.paddingOf(context).bottom, ), ], ), ], ), DetailNavBar( scrollProgress: scrollProgress, title: title, onBack: onBack, chromeColor: navigationColor ?? AndroidDetailColors.navBarBg, ), ], ), ); }, ); } } class _AndroidDetailBlurredBackdrop extends StatelessWidget { const _AndroidDetailBlurredBackdrop({ required this.imageUrl, required this.fallbackUrls, required this.embyBaseUrl, required this.imageHeaders, }); final String? imageUrl; final List fallbackUrls; final String embyBaseUrl; final Map? imageHeaders; static const double _blurSigma = 32; static const LinearGradient _scrim = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: [0.0, 0.4, 1.0], colors: [Color(0x59000000), Color(0x8C000000), Color(0xD9000000)], ); @override Widget build(BuildContext context) { final url = imageUrl; final Widget child; if (url == null) { child = const SizedBox.expand(key: ValueKey('__no_backdrop__')); } else { child = RepaintBoundary( key: ValueKey(url), child: Stack( fit: StackFit.expand, children: [ ImageFiltered( imageFilter: ImageFilter.blur( sigmaX: _blurSigma, sigmaY: _blurSigma, tileMode: TileMode.clamp, ), child: SmartImage( url: url, fallbackUrls: fallbackUrls, fit: BoxFit.cover, borderRadius: 0, memCacheWidth: 400, fallbackIcon: null, placeholder: const SizedBox.expand(), resolveHttpHeaders: (candidateUrl) => embyBaseUrl.isNotEmpty && candidateUrl.startsWith(embyBaseUrl) ? imageHeaders : null, ), ), const DecoratedBox(decoration: BoxDecoration(gradient: _scrim)), ], ), ); } return AnimatedSwitcher( duration: const Duration(milliseconds: 350), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, layoutBuilder: (currentChild, previousChildren) => Stack( fit: StackFit.expand, children: [...previousChildren, if (currentChild != null) currentChild], ), child: child, ); } }