Initial commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user