Files
plezy/lib/widgets/tv_spotlight_scaffold.dart
T

145 lines
5.1 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../media/media_hub.dart';
import '../media/media_item.dart';
import '../media/media_server_client.dart';
import '../navigation/main_screen_scope.dart';
import '../services/settings_service.dart';
import '../utils/debouncer.dart';
import '../utils/layout_constants.dart';
import 'tv_browse_rail.dart';
import 'tv_spotlight_background.dart';
class TvSpotlightController extends ValueNotifier<MediaItem?> {
TvSpotlightController({Duration settleDelay = const Duration(milliseconds: 150)})
: _settleDelay = settleDelay,
_debouncer = Debouncer(settleDelay),
super(null);
final Duration _settleDelay;
final Debouncer _debouncer;
void select(MediaItem item) {
void apply() {
if (value?.globalKey == item.globalKey) return;
value = item;
}
if (_settleDelay == Duration.zero) {
apply();
} else {
_debouncer.run(apply);
}
}
MediaItem? resolve(Iterable<MediaHub> hubs) {
MediaItem? fallback;
final current = value;
for (final hub in hubs) {
if (hub.items.isEmpty) continue;
fallback ??= hub.items.first;
if (current == null) continue;
for (final item in hub.items) {
if (item.globalKey == current.globalKey) return item;
}
}
return fallback;
}
@override
void dispose() {
_debouncer.dispose();
super.dispose();
}
}
typedef TvSpotlightClientResolver = MediaServerClient? Function(MediaItem? item);
/// Shared full-screen TV backdrop and foreground stack used by hub rails.
class TvSpotlightScaffold extends StatelessWidget {
const TvSpotlightScaffold({
super.key,
required this.hubs,
required this.spotlightListenable,
required this.resolveSpotlight,
required this.resolveClient,
required this.foreground,
this.hideSpoilers,
});
final List<MediaHub> hubs;
final ValueListenable<MediaItem?> spotlightListenable;
final MediaItem? Function() resolveSpotlight;
final TvSpotlightClientResolver resolveClient;
final Widget foreground;
final bool? hideSpoilers;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final settings = SettingsService.instance;
final scale = TvLayoutConstants.scaleForSize(size);
final railSize = MainScreenFocusScope.foregroundSizeOf(context);
final fullBleedWidth = MainScreenFocusScope.fullBleedWidthOf(context);
final railHeight = hubs.isEmpty
? 0.0
: TvBrowseRailLayout.estimateHeight(
size: railSize,
hubs: hubs,
density: settings.read(SettingsService.libraryDensity),
episodePosterMode: settings.read(SettingsService.episodePosterMode),
fullCardLayout: settings.read(SettingsService.tvFullCardLayout),
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
);
final spotlightTop = (size.height * 0.075).clamp(64.0 * scale, 120.0 * scale).toDouble();
final minimumSpotlightBottom = railHeight + (8 * scale);
final baseSpotlightBottom = (size.height * 0.48).clamp(160.0, 820.0).toDouble();
final desiredSpotlightBottom = minimumSpotlightBottom > baseSpotlightBottom
? minimumSpotlightBottom
: baseSpotlightBottom;
final maxSpotlightBottom = (size.height - spotlightTop - (96 * scale)).clamp(0.0, double.infinity).toDouble();
final spotlightBottom = desiredSpotlightBottom > maxSpotlightBottom ? maxSpotlightBottom : desiredSpotlightBottom;
final spotlightLeft = (24 * scale).clamp(18.0, 40.0).toDouble();
return Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: SizedBox.expand(
child: Stack(
fit: StackFit.expand,
clipBehavior: Clip.none,
children: [
Builder(
builder: (context) {
final foregroundLeft = MainScreenFocusScope.foregroundLeftOf(context);
return SideNavigationBleedBuilder(
targetBleed: foregroundLeft,
child: ValueListenableBuilder<MediaItem?>(
valueListenable: spotlightListenable,
builder: (context, _, _) {
final spotlight = resolveSpotlight();
return TvSpotlightBackground(
item: spotlight,
client: resolveClient(spotlight),
hideSpoilers: hideSpoilers ?? settings.read(SettingsService.hideSpoilers),
contentTop: spotlightTop,
contentBottom: spotlightBottom,
contentLeft: spotlightLeft + foregroundLeft,
compact: true,
showPrimaryAction: false,
);
},
),
builder: (context, animatedBleed, child) =>
Positioned(top: 0, bottom: 0, left: -animatedBleed, width: fullBleedWidth, child: child!),
);
},
),
foreground,
],
),
),
);
}
}