Files
sm-emby-share/lib/features/detail/widgets/detail_immersive_scaffold.dart
T

154 lines
4.6 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
import 'dart:io' show Platform;
import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'detail_backdrop.dart';
class DetailImmersiveScaffold extends StatelessWidget {
final ScrollController scrollController;
final ValueNotifier<double> scrollProgress;
final String? backdropUrl;
final List<String> fallbackUrls;
final String embyBaseUrl;
final Map<String, String>? imageHeaders;
final Widget child;
final bool compact;
final double horizontalPadding;
const DetailImmersiveScaffold({
super.key,
required this.scrollController,
required this.scrollProgress,
required this.child,
this.backdropUrl,
this.fallbackUrls = const <String>[],
this.embyBaseUrl = '',
this.imageHeaders,
this.compact = false,
this.horizontalPadding = 32,
});
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.sizeOf(context);
final screenHeight = screenSize.height;
final screenWidth = screenSize.width;
final bannerHeight = compact
? screenHeight * 0.30
: (screenWidth * 9.0 / 16.0).clamp(280.0, screenHeight * 0.6);
final backdrop = backdropUrl;
return Stack(
children: [
if (backdrop != null)
DetailBackdrop(
scrollProgress: scrollProgress,
backdropUrl: backdrop,
fallbackUrls: fallbackUrls,
embyBaseUrl: embyBaseUrl,
imageHeaders: imageHeaders,
),
SingleChildScrollView(
controller: scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: bannerHeight - 80),
Stack(
children: [
Positioned.fill(child: _FrostedPanelVeil()),
Padding(
padding: EdgeInsets.fromLTRB(
horizontalPadding,
64,
horizontalPadding,
48,
),
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1200),
child: child,
),
),
),
],
),
],
),
),
],
);
}
}
class _FrostedPanelVeil extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (Platform.isAndroid) {
return IgnorePointer(
child: ShaderMask(
shaderCallback: (rect) {
final ratio = (140.0 / rect.height).clamp(0.02, 0.5);
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: const [Colors.transparent, Colors.black],
stops: [0.0, ratio],
).createShader(rect);
},
blendMode: BlendMode.dstIn,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0, 0.12, 1.0],
colors: [
const Color(0xFF0a0c10).withValues(alpha: 0.0),
const Color(0xFF0a0c10).withValues(alpha: 0.88),
const Color(0xFF0a0c10).withValues(alpha: 0.96),
],
),
),
),
),
);
}
return IgnorePointer(
child: ShaderMask(
shaderCallback: (rect) {
final ratio = (140.0 / rect.height).clamp(0.02, 0.5);
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: const [Colors.transparent, Colors.black],
stops: [0.0, ratio],
).createShader(rect);
},
blendMode: BlendMode.dstIn,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0, 0.20, 1.0],
colors: [
const Color(0xFF0a0c10).withValues(alpha: 0.0),
const Color(0xFF0a0c10).withValues(alpha: 0.12),
const Color(0xFF0a0c10).withValues(alpha: 0.20),
],
),
),
),
),
),
);
}
}