Files
plezy/lib/widgets/focusable_media_card.dart
edde746 5fe1e6dc65 refactor(ui): delete unwired log output, dead notifier filters, and never-passed widget params
MemoryLogOutput extended LogOutput but was never wired as a Logger output (storage happens in the printer); DeletionNotifier/WatchStateNotifier's forServer/forItem filtered streams and WatchStateEvent.mediaType had no production consumers (tests now filter .stream directly); context.hiddenLibraries/profileSettings and toPlexUrl had zero call sites; MonoTokens.splashFactory was never read; OptimizedMediaImage's enableTranscoding/cacheKey chains, FocusableMediaCard.width/height/forceGridMode, TvBrowseRail's constant-zero gap functions, and media_image_helper's scaleFactor were never varied by any caller. The sha1 LRU stays (rebuild-hot artwork URLs); TvRailTrailing.none and the mono-theme copyWith stay live (lerp delegates to copyWith). Also carries the MusicPlayContext.id argument drops in the music screens and remaining sweep test updates.
2026-08-17 19:02:25 +02:00

162 lines
6.1 KiB
Dart

import 'package:flutter/material.dart';
import '../focus/focus_theme.dart';
import '../focus/focusable_wrapper.dart';
import '../media/media_item.dart';
import '../utils/platform_detector.dart';
import 'media_card.dart';
/// A focusable wrapper for MediaCard that handles D-pad navigation.
///
/// Wraps MediaCard with focus handling for TV/desktop navigation:
/// - Shows scale + border decoration when focused
/// - Handles SELECT key for activation with long-press detection
/// - Accepts optional external focusNode for programmatic focus control
class FocusableMediaCard extends StatefulWidget {
/// Either a [MediaItem] or a [MediaPlaylist]. Typed as [Object] because
/// Dart has no nominal union type. Forwarded as-is to the inner [MediaCard].
final Object item;
/// Optional row/column position announced with this card.
final String? semanticValue;
final void Function(MediaItem source)? onRefresh;
final VoidCallback? onRemoveFromContinueWatching;
final VoidCallback? onListRefresh;
final bool forceListMode;
final bool isInContinueWatching;
final bool usesContinueWatchingAction;
final String? collectionId;
/// True for downloaded content without server access
final bool isOffline;
/// True when in a hub with mixed content (movies + episodes)
final bool mixedHubContext;
/// Render grid cards as image-only full-bleed cards.
final bool fullBleedImage;
/// Overrides the silhouette inferred from [item]. Used for container types
/// such as music collections and playlists.
final CardShape? cardShapeOverride;
/// Show server name in list view (multi-server)
final bool showServerName;
/// Library name to attribute the item with in list view, resolved by the
/// caller (see `LibrariesProvider.libraryLabelFor`). Null renders nothing.
final String? libraryName;
/// Whether to disable the scale animation on focus (e.g. in list view).
final bool disableScale;
/// Optional external focus node for programmatic focus control.
/// If not provided, an internal focus node is created.
final FocusNode? focusNode;
/// Called when the user presses UP and there's no focusable item above.
/// Used to navigate from the top row to filter chips.
final VoidCallback? onNavigateUp;
/// Called when the user presses DOWN and there's no focusable item below.
/// When the grid wires explicit row navigation, this points at the item in
/// the next row (or null on the last row).
final VoidCallback? onNavigateDown;
/// Called when the user presses LEFT and there's no focusable item to the left.
/// Used to navigate from the first column to the sidebar.
final VoidCallback? onNavigateLeft;
/// Called when the user presses RIGHT and there's no focusable item to the right.
/// Used to navigate from the last column to the alpha jump bar.
final VoidCallback? onNavigateRight;
/// Called when the user presses BACK.
/// Used to navigate from tab content to tab bar.
final VoidCallback? onBack;
/// Called when focus changes.
/// Used to track which grid item was last focused.
final ValueChanged<bool>? onFocusChange;
const FocusableMediaCard({
super.key,
required this.item,
this.semanticValue,
this.onRefresh,
this.onRemoveFromContinueWatching,
this.onListRefresh,
this.forceListMode = false,
this.isInContinueWatching = false,
bool? usesContinueWatchingAction,
this.collectionId,
this.isOffline = false,
this.mixedHubContext = false,
this.fullBleedImage = false,
this.cardShapeOverride,
this.showServerName = false,
this.libraryName,
this.disableScale = false,
this.focusNode,
this.onNavigateUp,
this.onNavigateDown,
this.onNavigateLeft,
this.onNavigateRight,
this.onBack,
this.onFocusChange,
}) : usesContinueWatchingAction = usesContinueWatchingAction ?? isInContinueWatching;
@override
State<FocusableMediaCard> createState() => _FocusableMediaCardState();
}
class _FocusableMediaCardState extends State<FocusableMediaCard> {
final GlobalKey<MediaCardState> _mediaCardKey = GlobalKey();
@override
Widget build(BuildContext context) {
return FocusableWrapper(
focusNode: widget.focusNode,
// The MediaCard already exposes the complete static button semantics.
// Avoid invalidating a dense TV grid's semantics tree on every D-pad
// focus change unless an accessibility service needs focused state.
includeFocusSemantics: !PlatformDetector.isTV() || MediaQuery.accessibleNavigationOf(context),
onSelect: () => _mediaCardKey.currentState?.handleTap(),
onLongPress: () => _mediaCardKey.currentState?.showContextMenu(),
onNavigateUp: widget.onNavigateUp,
onNavigateDown: widget.onNavigateDown,
onNavigateLeft: widget.onNavigateLeft,
onNavigateRight: widget.onNavigateRight,
onBack: widget.onBack,
onFocusChange: widget.onFocusChange,
enableLongPress: true,
disableScale: widget.disableScale,
focusScale: widget.fullBleedImage ? FocusTheme.fullCardFocusScale : FocusTheme.focusScale,
useFocusGlow: widget.fullBleedImage,
// MediaCard draws the focus border itself, on the rect its layout
// highlights (poster for standard grid cards, whole card otherwise).
delegateFocusBorder: true,
useComfortableZone: !PlatformDetector.isTV(), // Always center on TV
scrollAlignment: 0.5,
child: MediaCard(
key: _mediaCardKey,
item: widget.item,
semanticValue: widget.semanticValue,
onRefresh: widget.onRefresh,
onRemoveFromContinueWatching: widget.onRemoveFromContinueWatching,
onListRefresh: widget.onListRefresh,
forceListMode: widget.forceListMode,
isInContinueWatching: widget.isInContinueWatching,
usesContinueWatchingAction: widget.usesContinueWatchingAction,
collectionId: widget.collectionId,
isOffline: widget.isOffline,
mixedHubContext: widget.mixedHubContext,
fullBleedImage: widget.fullBleedImage,
cardShapeOverride: widget.cardShapeOverride,
showServerName: widget.showServerName,
libraryName: widget.libraryName,
),
);
}
}