The search screens, the out-of-band auth dialogs, the live TV guide and the list-download paths each carried their own copy of the same shell. Extracts SearchInputField and PendingAuthDialog and routes the duplicated download and guide helpers through one implementation.
99 lines
3.1 KiB
Dart
99 lines
3.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:material_symbols_icons/symbols.dart';
|
|
|
|
import '../focus/focusable_button.dart';
|
|
import '../focus/focusable_text_field.dart';
|
|
import 'app_icon.dart';
|
|
import 'pill_input_decoration.dart';
|
|
|
|
/// The pill search field the search screens put above their results, with the
|
|
/// clear affordance that appears once there is text: RIGHT out of the field
|
|
/// lands on it, LEFT goes back, and both escape down into the results.
|
|
///
|
|
/// [onBack] stays null unless the host wants the back key — a pushed route
|
|
/// needs it for its own pop.
|
|
class SearchInputField extends StatefulWidget {
|
|
final TextEditingController controller;
|
|
final FocusNode focusNode;
|
|
final String hintText;
|
|
|
|
/// Names the clear button's focus node.
|
|
final String debugLabel;
|
|
|
|
final TvKeyboardController? tvKeyboardController;
|
|
final VoidCallback? onNavigateLeft;
|
|
final VoidCallback? onNavigateDown;
|
|
final VoidCallback? onEditingComplete;
|
|
final VoidCallback? onBack;
|
|
|
|
const SearchInputField({
|
|
super.key,
|
|
required this.controller,
|
|
required this.focusNode,
|
|
required this.hintText,
|
|
required this.debugLabel,
|
|
this.tvKeyboardController,
|
|
this.onNavigateLeft,
|
|
this.onNavigateDown,
|
|
this.onEditingComplete,
|
|
this.onBack,
|
|
});
|
|
|
|
@override
|
|
State<SearchInputField> createState() => _SearchInputFieldState();
|
|
}
|
|
|
|
class _SearchInputFieldState extends State<SearchInputField> {
|
|
late final FocusNode _clearFocusNode = FocusNode(debugLabel: '${widget.debugLabel}.clear');
|
|
|
|
@override
|
|
void dispose() {
|
|
_clearFocusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _clearSearch() {
|
|
widget.controller.clear();
|
|
widget.focusNode.requestFocus();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final hasText = widget.controller.text.isNotEmpty;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
|
child: Stack(
|
|
alignment: Alignment.centerRight,
|
|
children: [
|
|
FocusableTextField(
|
|
controller: widget.controller,
|
|
focusNode: widget.focusNode,
|
|
tvKeyboardController: widget.tvKeyboardController,
|
|
textInputAction: TextInputAction.search,
|
|
onNavigateLeft: widget.onNavigateLeft,
|
|
onNavigateRight: hasText ? _clearFocusNode.requestFocus : null,
|
|
onNavigateDown: widget.onNavigateDown,
|
|
onEditingComplete: widget.onEditingComplete,
|
|
onBack: widget.onBack,
|
|
decoration: pillInputDecoration(
|
|
context,
|
|
hintText: widget.hintText,
|
|
prefixIcon: const AppIcon(Symbols.search_rounded, fill: 1),
|
|
suffixIcon: hasText ? const SizedBox(width: 48) : null,
|
|
),
|
|
),
|
|
if (hasText)
|
|
FocusableButton(
|
|
focusNode: _clearFocusNode,
|
|
onPressed: _clearSearch,
|
|
onNavigateLeft: widget.focusNode.requestFocus,
|
|
onNavigateDown: widget.onNavigateDown,
|
|
autoScroll: false,
|
|
child: IconButton(icon: const AppIcon(Symbols.clear_rounded, fill: 1), onPressed: _clearSearch),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|