233 lines
7.2 KiB
Dart
233 lines
7.2 KiB
Dart
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<double> scrollProgress;
|
|
|
|
final String? backdropUrl;
|
|
final List<String> fallbackUrls;
|
|
|
|
final String embyBaseUrl;
|
|
final Map<String, String>? 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<Widget> 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<Color?>(
|
|
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<String> fallbackUrls;
|
|
final String embyBaseUrl;
|
|
final Map<String, String>? 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<String>('__no_backdrop__'));
|
|
} else {
|
|
child = RepaintBoundary(
|
|
key: ValueKey<String>(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,
|
|
);
|
|
}
|
|
}
|