Files
sm-emby-share/lib/shared/widgets/shell_backdrop_layer.dart
T
2026-07-14 11:11:36 +08:00

133 lines
4.0 KiB
Dart

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<String>(backdrop.primaryImageUrl),
state: backdrop,
imageHeaders: imageHeaders,
);
} else if (active) {
child = const DecoratedBox(
key: ValueKey<String>('__immersive_dark_fallback__'),
decoration: BoxDecoration(gradient: _immersiveDarkFallback),
);
} else {
child = ColoredBox(
key: ValueKey<String>('__surface__${scheme.brightness.name}'),
color: scheme.surface,
);
}
return AnimatedSwitcher(
duration: _crossfade,
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeOut,
layoutBuilder: (currentChild, previousChildren) {
final seen = <Key?>{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<String, String>? 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)],
),
),
),
],
);
}
}