refactor(ui): delete never-passed widget parameters and unreachable surfaces
Grep-verified dead surface across shared widgets: the app-menu system's minWidth/maxWidth/childPadding/alignmentOffset knobs threaded through four layers with no caller ever passing them; FocusableListTile's suppressInitialSelect/hoverColor/textColor/iconColor machinery (~55 lines) reachable by no call site; OptimizedMediaImage.playlist with zero constructions; TvSpotlightBackground's primary-action button that neither caller can render; MediaGridDelegate.createDelegate whose only caller was a test (MediaGridGeometry.resolve is the single delegate composition now); and the CustomAppBar->Config->DesktopTopBar chain collapsed onto DesktopSliverAppBar with its ~11 never-passed passthrough params (snap kept — real callers pass it).
This commit is contained in:
@@ -78,8 +78,6 @@ Future<T?> showAppMenu<T>(
|
||||
Rect? anchorRect,
|
||||
AppMenuAnchorAlignment anchorAlignment = AppMenuAnchorAlignment.start,
|
||||
bool focusFirstItem = false,
|
||||
double minWidth = 220,
|
||||
double? maxWidth,
|
||||
}) {
|
||||
assert(position != null || anchorRect != null, 'showAppMenu requires a position or anchorRect');
|
||||
|
||||
@@ -95,8 +93,6 @@ Future<T?> showAppMenu<T>(
|
||||
anchorRect: anchorRect,
|
||||
anchorAlignment: anchorAlignment,
|
||||
focusFirstItem: focusFirstItem,
|
||||
minWidth: minWidth,
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
transitionBuilder: (dialogContext, animation, _, child) {
|
||||
final curved = CurvedAnimation(parent: animation, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic);
|
||||
@@ -127,8 +123,6 @@ Future<T?> showAdaptiveAppMenu<T>(
|
||||
Rect? anchorRect,
|
||||
AppMenuAnchorAlignment anchorAlignment = AppMenuAnchorAlignment.start,
|
||||
bool focusFirstItem = false,
|
||||
double minWidth = 220,
|
||||
double? maxWidth,
|
||||
bool isScrollControlled = false,
|
||||
}) {
|
||||
// ThemeData.platform follows the real target platform by default, including
|
||||
@@ -150,8 +144,6 @@ Future<T?> showAdaptiveAppMenu<T>(
|
||||
anchorRect: anchorRect,
|
||||
anchorAlignment: anchorAlignment,
|
||||
focusFirstItem: focusFirstItem,
|
||||
minWidth: minWidth,
|
||||
maxWidth: maxWidth,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,10 +164,6 @@ class AppMenuButton<T> extends StatefulWidget {
|
||||
final AppMenuEntryBuilder<T> entriesBuilder;
|
||||
final ValueChanged<T>? onSelected;
|
||||
final AppMenuAnchorAlignment anchorAlignment;
|
||||
final Offset alignmentOffset;
|
||||
final double minWidth;
|
||||
final double? maxWidth;
|
||||
final EdgeInsetsGeometry? childPadding;
|
||||
|
||||
const AppMenuButton({
|
||||
super.key,
|
||||
@@ -186,10 +174,6 @@ class AppMenuButton<T> extends StatefulWidget {
|
||||
required this.entriesBuilder,
|
||||
this.onSelected,
|
||||
this.anchorAlignment = AppMenuAnchorAlignment.start,
|
||||
this.alignmentOffset = Offset.zero,
|
||||
this.minWidth = 220,
|
||||
this.maxWidth,
|
||||
this.childPadding,
|
||||
}) : assert(icon != null || child != null, 'AppMenuButton requires icon or child');
|
||||
|
||||
@override
|
||||
@@ -203,7 +187,7 @@ class AppMenuButtonState<T> extends State<AppMenuButton<T>> {
|
||||
final renderBox = context.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) return null;
|
||||
|
||||
final topLeft = renderBox.localToGlobal(Offset.zero) + widget.alignmentOffset;
|
||||
final topLeft = renderBox.localToGlobal(Offset.zero);
|
||||
final anchorRect = Rect.fromLTWH(topLeft.dx, topLeft.dy, renderBox.size.width, renderBox.size.height);
|
||||
final selected = await showAppMenu<T>(
|
||||
context,
|
||||
@@ -211,8 +195,6 @@ class AppMenuButtonState<T> extends State<AppMenuButton<T>> {
|
||||
anchorRect: anchorRect,
|
||||
anchorAlignment: widget.anchorAlignment,
|
||||
focusFirstItem: focusFirstItem,
|
||||
minWidth: widget.minWidth,
|
||||
maxWidth: widget.maxWidth,
|
||||
);
|
||||
if (!mounted || selected == null) return selected;
|
||||
widget.onSelected?.call(selected);
|
||||
@@ -227,13 +209,12 @@ class AppMenuButtonState<T> extends State<AppMenuButton<T>> {
|
||||
Widget build(BuildContext context) {
|
||||
final child = widget.child;
|
||||
if (child != null) {
|
||||
final content = Padding(padding: widget.childPadding ?? EdgeInsets.zero, child: child);
|
||||
final button = ClickableCursor(
|
||||
enabled: widget.enabled,
|
||||
child: InkWell(
|
||||
onTap: widget.enabled ? _handlePressed : null,
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: content,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
final tooltip = widget.tooltip;
|
||||
@@ -568,8 +549,6 @@ class _AppMenuPopup<T> extends StatefulWidget {
|
||||
final Rect? anchorRect;
|
||||
final AppMenuAnchorAlignment anchorAlignment;
|
||||
final bool focusFirstItem;
|
||||
final double minWidth;
|
||||
final double? maxWidth;
|
||||
|
||||
const _AppMenuPopup({
|
||||
required this.entries,
|
||||
@@ -577,8 +556,6 @@ class _AppMenuPopup<T> extends StatefulWidget {
|
||||
required this.anchorRect,
|
||||
required this.anchorAlignment,
|
||||
required this.focusFirstItem,
|
||||
required this.minWidth,
|
||||
required this.maxWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -586,15 +563,14 @@ class _AppMenuPopup<T> extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppMenuPopupState<T> extends State<_AppMenuPopup<T>> {
|
||||
static const double _minMenuWidth = 220;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.sizeOf(context);
|
||||
const edgePadding = 8.0;
|
||||
final desiredWidth = widget.maxWidth ?? math.max(widget.minWidth, _estimateMenuWidth(context));
|
||||
final menuWidth = desiredWidth.clamp(
|
||||
widget.minWidth,
|
||||
math.max(widget.minWidth, screenSize.width - edgePadding * 2),
|
||||
);
|
||||
final desiredWidth = math.max(_minMenuWidth, _estimateMenuWidth(context));
|
||||
final menuWidth = desiredWidth.clamp(_minMenuWidth, math.max(_minMenuWidth, screenSize.width - edgePadding * 2));
|
||||
final estimatedHeight = _estimateMenuHeight(widget.entries);
|
||||
final availableHeight = math.max(0.0, screenSize.height - edgePadding * 2);
|
||||
final menuHeight = estimatedHeight.clamp(0.0, availableHeight).toDouble();
|
||||
@@ -682,7 +658,7 @@ class _AppMenuPopupState<T> extends State<_AppMenuPopup<T>> {
|
||||
longest = math.max(longest, entry.label?.length ?? 0);
|
||||
}
|
||||
}
|
||||
return math.min(360, math.max(widget.minWidth, 96 + longest * 7.5));
|
||||
return math.min(360, math.max(_minMenuWidth, 96 + longest * 7.5));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,38 +3,6 @@ import '../utils/desktop_window_padding.dart';
|
||||
import '../services/fullscreen_state_manager.dart';
|
||||
import 'app_bar_back_button.dart';
|
||||
|
||||
/// Configuration class for common app bar properties.
|
||||
/// Reduces duplication between different app bar implementations.
|
||||
class DesktopAppBarConfig {
|
||||
final Widget? title;
|
||||
final List<Widget>? actions;
|
||||
final double? elevation;
|
||||
final Color? backgroundColor;
|
||||
final Color? surfaceTintColor;
|
||||
final Color? shadowColor;
|
||||
final double? scrolledUnderElevation;
|
||||
final bool floating;
|
||||
final bool pinned;
|
||||
final double? expandedHeight;
|
||||
final Widget? flexibleSpace;
|
||||
final PreferredSizeWidget? bottom;
|
||||
|
||||
const DesktopAppBarConfig({
|
||||
this.title,
|
||||
this.actions,
|
||||
this.elevation,
|
||||
this.backgroundColor,
|
||||
this.surfaceTintColor,
|
||||
this.shadowColor,
|
||||
this.scrolledUnderElevation,
|
||||
this.floating = false,
|
||||
this.pinned = false,
|
||||
this.expandedHeight,
|
||||
this.flexibleSpace,
|
||||
this.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
/// Helper class for building app bar sections with consistent desktop behavior.
|
||||
class DesktopAppBarSections {
|
||||
/// Builds the leading section with proper padding and back button handling.
|
||||
@@ -149,21 +117,16 @@ class DesktopSliverAppBar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified widget for desktop top bars that handles fullscreen state and back button logic.
|
||||
/// Reduces UI drift by centralizing the app bar implementation.
|
||||
class DesktopTopBar extends StatelessWidget {
|
||||
final DesktopAppBarConfig config;
|
||||
final Widget? leading;
|
||||
final VoidCallback? onBackPressed;
|
||||
/// Convenience wrapper around [DesktopSliverAppBar] — the canonical desktop app bar —
|
||||
/// that rebuilds when fullscreen state changes so window-control spacing stays correct.
|
||||
/// For anything beyond title/actions/pinned, use [DesktopSliverAppBar] directly.
|
||||
class CustomAppBar extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final List<Widget>? actions;
|
||||
final bool pinned;
|
||||
final bool automaticallyImplyLeading;
|
||||
|
||||
const DesktopTopBar({
|
||||
super.key,
|
||||
required this.config,
|
||||
this.leading,
|
||||
this.onBackPressed,
|
||||
this.automaticallyImplyLeading = true,
|
||||
});
|
||||
const CustomAppBar({super.key, this.title, this.actions, this.pinned = false, this.automaticallyImplyLeading = true});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -172,95 +135,14 @@ class DesktopTopBar extends StatelessWidget {
|
||||
builder: (context, _) {
|
||||
final isFullscreen = FullscreenStateManager().isFullscreen;
|
||||
|
||||
Widget? effectiveLeading = leading;
|
||||
if (effectiveLeading == null && automaticallyImplyLeading) {
|
||||
final parentRoute = ModalRoute.of(context);
|
||||
final canPop = parentRoute?.canPop ?? false;
|
||||
|
||||
if (canPop) {
|
||||
effectiveLeading = AppBarBackButton(style: BackButtonStyle.plain, onPressed: onBackPressed);
|
||||
}
|
||||
}
|
||||
|
||||
return DesktopSliverAppBar(
|
||||
key: ValueKey('desktop_top_bar_$isFullscreen'),
|
||||
title: config.title,
|
||||
actions: config.actions,
|
||||
leading: effectiveLeading,
|
||||
automaticallyImplyLeading: false,
|
||||
elevation: config.elevation,
|
||||
backgroundColor: config.backgroundColor,
|
||||
surfaceTintColor: config.surfaceTintColor,
|
||||
shadowColor: config.shadowColor,
|
||||
scrolledUnderElevation: config.scrolledUnderElevation,
|
||||
floating: config.floating,
|
||||
pinned: config.pinned,
|
||||
expandedHeight: config.expandedHeight,
|
||||
flexibleSpace: config.flexibleSpace,
|
||||
bottom: config.bottom,
|
||||
title: title,
|
||||
actions: actions,
|
||||
pinned: pinned,
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenient wrapper for DesktopSliverAppBar with built-in back button handling.
|
||||
///
|
||||
/// This widget is maintained for backward compatibility. For new code, consider
|
||||
/// using [DesktopTopBar] directly for a more unified approach.
|
||||
class CustomAppBar extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final List<Widget>? actions;
|
||||
final VoidCallback? onBackPressed;
|
||||
final double? elevation;
|
||||
final Color? backgroundColor;
|
||||
final Color? surfaceTintColor;
|
||||
final Color? shadowColor;
|
||||
final double? scrolledUnderElevation;
|
||||
final bool floating;
|
||||
final bool pinned;
|
||||
final double? expandedHeight;
|
||||
final Widget? flexibleSpace;
|
||||
final PreferredSizeWidget? bottom;
|
||||
final bool automaticallyImplyLeading;
|
||||
|
||||
const CustomAppBar({
|
||||
super.key,
|
||||
this.title,
|
||||
this.actions,
|
||||
this.onBackPressed,
|
||||
this.elevation,
|
||||
this.backgroundColor,
|
||||
this.surfaceTintColor,
|
||||
this.shadowColor,
|
||||
this.scrolledUnderElevation,
|
||||
this.floating = false,
|
||||
this.pinned = false,
|
||||
this.expandedHeight,
|
||||
this.flexibleSpace,
|
||||
this.bottom,
|
||||
this.automaticallyImplyLeading = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopTopBar(
|
||||
config: DesktopAppBarConfig(
|
||||
title: title,
|
||||
actions: actions,
|
||||
elevation: elevation,
|
||||
backgroundColor: backgroundColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
shadowColor: shadowColor,
|
||||
scrolledUnderElevation: scrolledUnderElevation,
|
||||
floating: floating,
|
||||
pinned: pinned,
|
||||
expandedHeight: expandedHeight,
|
||||
flexibleSpace: flexibleSpace,
|
||||
bottom: bottom,
|
||||
),
|
||||
onBackPressed: onBackPressed,
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../focus/dpad_navigator.dart';
|
||||
import '../focus/focusable_tile_mixin.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import 'clickable_cursor.dart';
|
||||
@@ -34,15 +33,6 @@ class FocusableListTile extends StatefulWidget {
|
||||
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
/// If true, consumes the first select key event to avoid accidental activation.
|
||||
final bool suppressInitialSelect;
|
||||
|
||||
final Color? hoverColor;
|
||||
|
||||
final Color? textColor;
|
||||
|
||||
final Color? iconColor;
|
||||
|
||||
final VisualDensity? visualDensity;
|
||||
|
||||
final double? horizontalTitleGap;
|
||||
@@ -63,10 +53,6 @@ class FocusableListTile extends StatefulWidget {
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.contentPadding,
|
||||
this.suppressInitialSelect = false,
|
||||
this.hoverColor,
|
||||
this.textColor,
|
||||
this.iconColor,
|
||||
this.visualDensity = const VisualDensity(vertical: -3),
|
||||
this.horizontalTitleGap,
|
||||
this.minLeadingWidth,
|
||||
@@ -77,27 +63,17 @@ class FocusableListTile extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FocusableListTileState extends State<FocusableListTile> with FocusableTileStateMixin<FocusableListTile> {
|
||||
bool _suppressionConsumed = false;
|
||||
bool _isHoveredOrFocused = false;
|
||||
|
||||
@override
|
||||
FocusNode? get widgetFocusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// When hovered/focused with a custom hoverColor, use onError-style foreground
|
||||
// to keep text readable against the colored background.
|
||||
final needsContrastSwap = _isHoveredOrFocused && widget.hoverColor != null && widget.textColor != null;
|
||||
final textColor = needsContrastSwap ? Theme.of(context).colorScheme.onError : widget.textColor;
|
||||
final iconColor = needsContrastSwap ? Theme.of(context).colorScheme.onError : widget.iconColor;
|
||||
final automotive = PlatformDetector.isAutomotive();
|
||||
|
||||
final Widget tile = MouseRegion(
|
||||
return MouseRegion(
|
||||
cursor: widget.enabled && (widget.onTap != null || widget.onLongPress != null)
|
||||
? SystemMouseCursors.click
|
||||
: MouseCursor.defer,
|
||||
onEnter: widget.hoverColor != null ? (_) => setState(() => _isHoveredOrFocused = true) : null,
|
||||
onExit: widget.hoverColor != null ? (_) => setState(() => _isHoveredOrFocused = false) : null,
|
||||
child: ListTile(
|
||||
title: widget.title,
|
||||
subtitle: widget.subtitle,
|
||||
@@ -110,35 +86,12 @@ class _FocusableListTileState extends State<FocusableListTile> with FocusableTil
|
||||
selected: widget.selected,
|
||||
contentPadding: widget.contentPadding,
|
||||
visualDensity: automotive ? VisualDensity.standard : widget.visualDensity,
|
||||
focusNode: widget.suppressInitialSelect ? null : effectiveFocusNode,
|
||||
autofocus: widget.suppressInitialSelect ? false : widget.autofocus,
|
||||
hoverColor: widget.hoverColor,
|
||||
textColor: textColor,
|
||||
iconColor: iconColor,
|
||||
focusNode: effectiveFocusNode,
|
||||
autofocus: widget.autofocus,
|
||||
horizontalTitleGap: widget.horizontalTitleGap,
|
||||
minLeadingWidth: widget.minLeadingWidth,
|
||||
),
|
||||
);
|
||||
|
||||
if (!widget.suppressInitialSelect) {
|
||||
return tile;
|
||||
}
|
||||
|
||||
return Focus(
|
||||
focusNode: effectiveFocusNode,
|
||||
autofocus: widget.autofocus,
|
||||
onKeyEvent: (node, event) {
|
||||
if (SelectKeyUpSuppressor.consumeIfSuppressed(event)) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (!_suppressionConsumed && event.logicalKey.isSelectKey) {
|
||||
_suppressionConsumed = true;
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: tile,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,6 @@ class FocusablePopupMenuButton<T> extends StatefulWidget {
|
||||
final ValueChanged<T>? onSelected;
|
||||
final GlobalKey<AppMenuButtonState<T>>? menuKey;
|
||||
final AppMenuAnchorAlignment anchorAlignment;
|
||||
final Offset alignmentOffset;
|
||||
final double minWidth;
|
||||
final double? maxWidth;
|
||||
final EdgeInsetsGeometry? childPadding;
|
||||
final FocusNode? focusNode;
|
||||
final VoidCallback? onNavigateUp;
|
||||
final VoidCallback? onNavigateDown;
|
||||
@@ -40,10 +36,6 @@ class FocusablePopupMenuButton<T> extends StatefulWidget {
|
||||
this.onSelected,
|
||||
this.menuKey,
|
||||
this.anchorAlignment = AppMenuAnchorAlignment.start,
|
||||
this.alignmentOffset = Offset.zero,
|
||||
this.minWidth = 220,
|
||||
this.maxWidth,
|
||||
this.childPadding,
|
||||
this.focusNode,
|
||||
this.onNavigateUp,
|
||||
this.onNavigateDown,
|
||||
@@ -93,10 +85,6 @@ class _FocusablePopupMenuButtonState<T> extends State<FocusablePopupMenuButton<T
|
||||
onSelected: widget.onSelected,
|
||||
entriesBuilder: widget.itemBuilder,
|
||||
anchorAlignment: widget.anchorAlignment,
|
||||
alignmentOffset: widget.alignmentOffset,
|
||||
minWidth: widget.minWidth,
|
||||
maxWidth: widget.maxWidth,
|
||||
childPadding: widget.childPadding,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,61 +4,16 @@ import '../utils/grid_size_calculator.dart';
|
||||
import '../utils/layout_constants.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
|
||||
/// Shared grid delegate configuration for media item grids
|
||||
/// Maintains consistent aspect ratio and spacing across all media grids.
|
||||
/// Shared grid metric helpers for media item grids — spacing, aspect ratio,
|
||||
/// and max cross-axis extent. [MediaGridGeometry.resolve] is the single place
|
||||
/// that composes them into a grid delegate.
|
||||
class MediaGridDelegate {
|
||||
/// Creates a standard grid delegate for media items
|
||||
///
|
||||
/// Uses [GridSizeCalculator.getMaxCrossAxisExtent] by default.
|
||||
/// Set [usePaddingAware] to true to use [GridSizeCalculator.getMaxCrossAxisExtentWithPadding] instead.
|
||||
/// Set [useWideAspectRatio] to true to use 16:9 aspect ratio for episode thumbnails.
|
||||
/// Pass [shape] to select the cell silhouette directly — it wins over
|
||||
/// [useWideAspectRatio]; square cells keep the poster max extent.
|
||||
/// Set [fullBleedImage] to true when the card is image-only and should not reserve text height.
|
||||
/// Pass [maxCrossAxisExtentOverride] to bypass the calculator and the wide-aspect multiplier —
|
||||
/// the caller is then responsible for providing a fully-resolved per-cell width.
|
||||
static SliverGridDelegateWithMaxCrossAxisExtent createDelegate({
|
||||
required BuildContext context,
|
||||
required int density,
|
||||
bool usePaddingAware = false,
|
||||
double horizontalPadding = 16,
|
||||
bool useWideAspectRatio = false,
|
||||
bool fullBleedImage = false,
|
||||
CardShape? shape,
|
||||
double? maxCrossAxisExtentOverride,
|
||||
}) {
|
||||
final aspectRatio = aspectRatioFor(
|
||||
useWideAspectRatio: useWideAspectRatio,
|
||||
fullBleedImage: fullBleedImage,
|
||||
shape: shape,
|
||||
);
|
||||
final spacing = spacingFor(context: context, fullBleedImage: fullBleedImage);
|
||||
|
||||
final maxCrossAxisExtent =
|
||||
maxCrossAxisExtentOverride ??
|
||||
_maxCrossAxisExtentFor(
|
||||
context: context,
|
||||
density: density,
|
||||
usePaddingAware: usePaddingAware,
|
||||
horizontalPadding: horizontalPadding,
|
||||
useWideAspectRatio: useWideAspectRatio,
|
||||
shape: shape,
|
||||
);
|
||||
|
||||
return SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||
childAspectRatio: aspectRatio,
|
||||
crossAxisSpacing: spacing,
|
||||
mainAxisSpacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolves the shape from the optional [shape] parameter, falling back to
|
||||
/// the legacy wide-vs-poster bool so existing call sites are byte-identical.
|
||||
static CardShape _resolveShape(CardShape? shape, bool useWideAspectRatio) =>
|
||||
shape ?? (useWideAspectRatio ? CardShape.wide : CardShape.poster);
|
||||
|
||||
/// Resolves the max cross-axis extent the way [createDelegate] does,
|
||||
/// Resolves the max cross-axis extent for [MediaGridGeometry.resolve],
|
||||
/// including the 1.8x widening for 16:9 episode thumbnails. Square cells
|
||||
/// keep the poster extent so column counts match the poster grid.
|
||||
static double _maxCrossAxisExtentFor({
|
||||
|
||||
@@ -192,43 +192,6 @@ class OptimizedMediaImage extends StatelessWidget {
|
||||
artworkDim: artworkDim,
|
||||
);
|
||||
|
||||
/// Named constructor for playlist images.
|
||||
const OptimizedMediaImage.playlist({
|
||||
Key? key,
|
||||
MediaServerClient? client,
|
||||
required String? imagePath,
|
||||
double? width,
|
||||
double? height,
|
||||
BoxFit fit = BoxFit.cover,
|
||||
FilterQuality filterQuality = FilterQuality.medium,
|
||||
Widget Function(BuildContext, String)? placeholder,
|
||||
Widget Function(BuildContext, String, dynamic)? errorWidget,
|
||||
Duration fadeInDuration = const Duration(milliseconds: 300),
|
||||
bool enableTranscoding = true,
|
||||
String? cacheKey,
|
||||
Alignment alignment = Alignment.center,
|
||||
String? localFilePath,
|
||||
Animation<double>? artworkDim,
|
||||
}) : this._(
|
||||
key: key,
|
||||
client: client,
|
||||
imagePath: imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
filterQuality: filterQuality,
|
||||
placeholder: placeholder,
|
||||
errorWidget: errorWidget,
|
||||
fadeInDuration: fadeInDuration,
|
||||
enableTranscoding: enableTranscoding,
|
||||
cacheKey: cacheKey,
|
||||
alignment: alignment,
|
||||
fallbackIcon: Symbols.playlist_play_rounded,
|
||||
imageType: ImageType.poster,
|
||||
localFilePath: localFilePath,
|
||||
artworkDim: artworkDim,
|
||||
);
|
||||
|
||||
/// Whether both width and height are explicitly set to finite positive values,
|
||||
/// meaning we can skip the LayoutBuilder.
|
||||
bool get _hasKnownDimensions =>
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../media/media_item_types.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
import '../providers/watch_state_store.dart';
|
||||
import '../services/device_performance.dart';
|
||||
import '../utils/content_utils.dart';
|
||||
import '../utils/formatters.dart';
|
||||
import '../utils/layout_constants.dart';
|
||||
import '../utils/media_image_helper.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import 'app_icon.dart';
|
||||
import 'cycling_media_backdrop.dart';
|
||||
import 'fitting_title_text.dart';
|
||||
import 'fitted_metadata_line.dart';
|
||||
@@ -30,10 +27,7 @@ class TvSpotlightBackground extends StatelessWidget {
|
||||
final double contentBottom;
|
||||
final double? contentTop;
|
||||
final double? contentLeft;
|
||||
final VoidCallback? onPrimaryAction;
|
||||
final Widget? actions;
|
||||
final bool compact;
|
||||
final bool showPrimaryAction;
|
||||
final bool showInfo;
|
||||
final String? Function(String? artworkPath)? localArtworkPathResolver;
|
||||
final bool allowNetwork;
|
||||
@@ -49,10 +43,7 @@ class TvSpotlightBackground extends StatelessWidget {
|
||||
this.contentBottom = 360,
|
||||
this.contentTop,
|
||||
this.contentLeft,
|
||||
this.onPrimaryAction,
|
||||
this.actions,
|
||||
this.compact = false,
|
||||
this.showPrimaryAction = true,
|
||||
this.showInfo = true,
|
||||
this.localArtworkPathResolver,
|
||||
this.allowNetwork = true,
|
||||
@@ -228,10 +219,6 @@ class TvSpotlightBackground extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (showPrimaryAction || actions != null) ...[
|
||||
SizedBox(height: (compact ? 18 : 26) * scale),
|
||||
actions ?? _buildPrimaryAction(context, media),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -369,32 +356,4 @@ class TvSpotlightBackground extends StatelessWidget {
|
||||
double _metadataFontSize(double scale) => (compact ? 16 : 18) * scale;
|
||||
|
||||
double _summaryFontSize(double scale) => (compact ? 18 : 20) * scale;
|
||||
|
||||
Widget _buildPrimaryAction(BuildContext context, MediaItem media) {
|
||||
final scale = _scale(context);
|
||||
media = context.withFreshWatchState(media);
|
||||
final hasProgress = media.hasActiveProgress;
|
||||
final minutesLeft = hasProgress && media.durationMs != null && media.viewOffsetMs != null
|
||||
? ((media.durationMs! - media.viewOffsetMs!) / 60_000).round()
|
||||
: 0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onPrimaryAction,
|
||||
child: Container(
|
||||
padding: .symmetric(horizontal: (compact ? 24 : 30) * scale, vertical: (compact ? 12 : 15) * scale),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(32 * scale)),
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
AppIcon(Symbols.play_arrow_rounded, fill: 1, size: (compact ? 24 : 28) * scale, color: Colors.black),
|
||||
SizedBox(width: (compact ? 10 : 12) * scale),
|
||||
Text(
|
||||
hasProgress ? t.discover.minutesLeft(minutes: minutesLeft) : t.common.play,
|
||||
style: TextStyle(color: Colors.black, fontSize: (compact ? 16 : 18) * scale, fontWeight: .w800),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,6 @@ class _CatalogSpotlightBackgroundState extends State<_CatalogSpotlightBackground
|
||||
contentBottom: widget.contentBottom,
|
||||
contentLeft: widget.contentLeft,
|
||||
compact: true,
|
||||
showPrimaryAction: false,
|
||||
metadataTrailing: _buildNextEpisodeMetadata(context),
|
||||
);
|
||||
if (accentColor == null) return background;
|
||||
|
||||
@@ -66,11 +66,12 @@ void main() {
|
||||
_TestApp(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
delegate = MediaGridDelegate.createDelegate(
|
||||
delegate = MediaGridGeometry.resolve(
|
||||
context: context,
|
||||
crossAxisExtent: 1280,
|
||||
density: LibraryDensity.defaultValue,
|
||||
fullBleedImage: true,
|
||||
);
|
||||
).delegate;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
|
||||
@@ -33,7 +33,6 @@ Future<void> _pumpSpotlight(WidgetTester tester, MediaItem item) async {
|
||||
item: item,
|
||||
client: null,
|
||||
allowNetwork: false,
|
||||
showPrimaryAction: false,
|
||||
compact: true,
|
||||
contentTop: 80,
|
||||
contentBottom: 200,
|
||||
|
||||
Reference in New Issue
Block a user