Files
plezy/lib/widgets/focus_builders.dart
T
edde746 4eaf4423a1 refactor: share focus chrome and simplify the TV picker and browse paths
Focus chrome was implemented twice, once in the focusable wrapper and once
in the focus builders; both now go through FocusChrome. TvColorPicker's
channel row was a copy of TvNumberSpinner and is now that widget in compact
density.

Also trims unused helpers and fields and simplifies the Jellyfin browse
paths.
2026-07-26 06:09:49 +02:00

129 lines
4.5 KiB
Dart

import 'package:flutter/material.dart';
import '../focus/focus_chrome.dart';
import '../focus/focus_theme.dart';
import '../focus/input_mode_tracker.dart';
import 'clickable_cursor.dart';
/// Shared builders for focusable widgets to reduce code duplication.
///
/// These builders provide consistent focus decoration patterns across
/// different focusable widgets (chips, cards, etc.).
class FocusBuilders {
/// Builds a chip-style focusable widget with background color changes.
///
/// Used by FocusableTabChip and FocusableFilterChip.
///
/// Parameters:
/// - [context]: Build context for theming
/// - [focusNode]: The focus node for this widget
/// - [isFocused]: Whether this widget currently has focus
/// - [onKeyEvent]: Callback for handling key events
/// - [onTap]: Callback for tap/click events
/// - [padding]: Padding inside the chip
/// - [backgroundColor]: Background color for the chip
/// - [borderRadius]: Border radius for the chip (defaults to 20)
/// - [child]: The content to display inside the chip
static Widget buildFocusableChip({
required BuildContext context,
required FocusNode focusNode,
required KeyEventResult Function(FocusNode, KeyEvent) onKeyEvent,
required VoidCallback onTap,
required String semanticLabel,
bool? selected,
required EdgeInsetsGeometry padding,
required Color backgroundColor,
double borderRadius = 20,
required Widget child,
}) {
final duration = FocusTheme.getAnimationDuration(context);
return Focus(
focusNode: focusNode,
onKeyEvent: onKeyEvent,
child: Semantics(
label: semanticLabel,
button: true,
selected: selected,
onTap: onTap,
excludeSemantics: true,
child: ClickableCursor(
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: duration,
curve: Curves.easeOutCubic,
padding: padding,
decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(borderRadius)),
child: child,
),
),
),
),
);
}
/// Builds a card-style wrapper with scale and border decoration but no [Focus]
/// node — focus lives on an enclosing rail or screen that passes [isFocused]
/// down.
///
/// Used by the hub row, the TV browse rail, the cast strip and the extras row.
/// Cards that own their focus node use [FocusableWrapper] instead; both share
/// the same chrome through [buildFocusChrome].
///
/// Parameters:
/// - [context]: Build context for theming
/// - [isFocused]: Whether this widget should appear focused
/// - [onTap]: Callback for tap/click events
/// - [onLongPress]: Callback for long press events
/// - [borderRadius]: Border radius for the focus decoration
/// - [child]: The content to display inside the wrapper
static Widget buildLockedFocusWrapper({
required BuildContext context,
required bool isFocused,
VoidCallback? onTap,
VoidCallback? onLongPress,
double borderRadius = FocusTheme.defaultBorderRadius,
double focusScale = FocusTheme.focusScale,
bool useFocusGlow = false,
bool delegateFocusBorder = false,
Size? glowSize,
required Widget child,
}) {
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
// In touch mode, no item ever shows focus effects — skip animated wrappers
// entirely. This saves ~2 element levels per card on ARM32 Android phones.
if (!isKeyboardMode) {
return (onTap != null || onLongPress != null)
? ClickableCursor(
child: GestureDetector(onTap: onTap, onLongPress: onLongPress, child: child),
)
: child;
}
final duration = FocusTheme.getAnimationDuration(context);
final focusedWidget = AnimatedScale(
scale: isFocused ? focusScale : 1.0,
duration: duration,
curve: Curves.easeOutCubic,
child: buildFocusChrome(
context,
showFocus: isFocused,
duration: duration,
borderRadius: borderRadius,
useFocusGlow: useFocusGlow,
delegateFocusBorder: delegateFocusBorder,
glowSize: glowSize,
child: child,
),
);
// Wrap in GestureDetector if tap/long press handlers provided
return (onTap != null || onLongPress != null)
? ClickableCursor(
child: GestureDetector(onTap: onTap, onLongPress: onLongPress, child: focusedWidget),
)
: focusedWidget;
}
}