tv: search, info

This commit is contained in:
Thibault Deckers 2023-01-18 14:58:24 +01:00
parent 7cf5538408
commit 54aa981fd1
9 changed files with 125 additions and 42 deletions

View file

@ -5,11 +5,21 @@ import 'package:provider/provider.dart';
// to be placed at the edges of lists and grids,
// so that TV can reach them with D-pad
class TvEdgeFocus extends StatelessWidget {
const TvEdgeFocus({super.key});
final FocusNode? focusNode;
const TvEdgeFocus({
super.key,
this.focusNode,
});
@override
Widget build(BuildContext context) {
final useTvLayout = context.select<Settings, bool>((s) => s.useTvLayout);
return useTvLayout ? const Focus(child: SizedBox()) : const SizedBox();
return useTvLayout
? Focus(
focusNode: focusNode,
child: const SizedBox(),
)
: const SizedBox();
}
}

View file

@ -1,4 +1,5 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart';
@ -31,26 +32,49 @@ class TitledExpandableFilterRow extends StatelessWidget {
final isExpanded = expandedNotifier.value == title;
Widget header = Text(
title,
style: Constants.knownTitleTextStyle,
);
void toggle() => expandedNotifier.value = isExpanded ? null : title;
if (settings.useTvLayout) {
header = Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: InkWell(
onTap: toggle,
borderRadius: const BorderRadius.all(Radius.circular(123)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
header,
],
),
),
),
);
} else {
header = Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
header,
const Spacer(),
IconButton(
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
onPressed: toggle,
tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
title,
style: Constants.knownTitleTextStyle,
),
const Spacer(),
IconButton(
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
onPressed: () => expandedNotifier.value = isExpanded ? null : title,
tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
),
],
),
),
header,
ExpandableFilterRow(
filters: filters,
isExpanded: isExpanded,

View file

@ -18,6 +18,9 @@ abstract class AvesSearchDelegate extends SearchDelegate {
query = initialQuery ?? '';
}
@mustCallSuper
void dispose() {}
@override
Widget? buildLeading(BuildContext context) {
if (settings.useTvLayout) {
@ -44,7 +47,7 @@ abstract class AvesSearchDelegate extends SearchDelegate {
@override
List<Widget>? buildActions(BuildContext context) {
return [
if (query.isNotEmpty)
if (!settings.useTvLayout && query.isNotEmpty)
IconButton(
icon: const Icon(AIcons.clear),
onPressed: () {
@ -63,28 +66,40 @@ abstract class AvesSearchDelegate extends SearchDelegate {
void clean() {
currentBody = null;
focusNode?.unfocus();
searchFieldFocusNode?.unfocus();
}
// adapted from Flutter `SearchDelegate` in `/material/search.dart`
@override
void showResults(BuildContext context) {
focusNode?.unfocus();
currentBody = SearchBody.results;
if (settings.useTvLayout) {
suggestionsScrollController?.jumpTo(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
suggestionsFocusNode?.requestFocus();
FocusScope.of(context).nextFocus();
});
} else {
searchFieldFocusNode?.unfocus();
currentBody = SearchBody.results;
}
}
@override
void showSuggestions(BuildContext context) {
assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.');
focusNode!.requestFocus();
assert(searchFieldFocusNode != null, '_focusNode must be set by route before showSuggestions is called.');
searchFieldFocusNode!.requestFocus();
currentBody = SearchBody.suggestions;
}
@override
Animation<double> get transitionAnimation => proxyAnimation;
FocusNode? focusNode;
FocusNode? searchFieldFocusNode;
FocusNode? get suggestionsFocusNode => null;
ScrollController? get suggestionsScrollController => null;
final TextEditingController queryTextController = TextEditingController();

View file

@ -29,7 +29,7 @@ class SearchPage extends StatefulWidget {
class _SearchPageState extends State<SearchPage> {
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
final FocusNode _focusNode = FocusNode();
final FocusNode _searchFieldFocusNode = FocusNode();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
@ -37,7 +37,7 @@ class _SearchPageState extends State<SearchPage> {
super.initState();
_registerWidget(widget);
widget.animation.addStatusListener(_onAnimationStatusChanged);
_focusNode.addListener(_onFocusChanged);
_searchFieldFocusNode.addListener(_onFocusChanged);
}
@override
@ -53,21 +53,22 @@ class _SearchPageState extends State<SearchPage> {
void dispose() {
_unregisterWidget(widget);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
_focusNode.dispose();
_searchFieldFocusNode.dispose();
_doubleBackPopHandler.dispose();
widget.delegate.dispose();
super.dispose();
}
void _registerWidget(SearchPage widget) {
widget.delegate.queryTextController.addListener(_onQueryChanged);
widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged);
widget.delegate.focusNode = _focusNode;
widget.delegate.searchFieldFocusNode = _searchFieldFocusNode;
}
void _unregisterWidget(SearchPage widget) {
widget.delegate.queryTextController.removeListener(_onQueryChanged);
widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate.focusNode = null;
widget.delegate.searchFieldFocusNode = null;
}
void _onAnimationStatusChanged(AnimationStatus status) {
@ -77,12 +78,12 @@ class _SearchPageState extends State<SearchPage> {
widget.animation.removeStatusListener(_onAnimationStatusChanged);
Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) {
if (!mounted) return;
_focusNode.requestFocus();
_searchFieldFocusNode.requestFocus();
});
}
void _onFocusChanged() {
if (_focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) {
if (_searchFieldFocusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) {
widget.delegate.showSuggestions(context);
}
}
@ -136,7 +137,7 @@ class _SearchPageState extends State<SearchPage> {
style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]),
child: TextField(
controller: widget.delegate.queryTextController,
focusNode: _focusNode,
focusNode: _searchFieldFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.delegate.searchFieldLabel,

View file

@ -19,6 +19,7 @@ import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/basic/tv_edge_focus.dart';
import 'package:aves/widgets/common/expandable_filter_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
@ -33,6 +34,14 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
final CollectionSource source;
final CollectionLens? parentCollection;
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
final FocusNode _suggestionsTopFocusNode = FocusNode();
final ScrollController _suggestionsScrollController = ScrollController();
@override
FocusNode? get suggestionsFocusNode => _suggestionsTopFocusNode;
@override
ScrollController get suggestionsScrollController => _suggestionsScrollController;
static const int searchHistoryCount = 10;
static final typeFilters = [
@ -64,6 +73,14 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
query = initialQuery ?? '';
}
@override
void dispose() {
_expandedSectionNotifier.dispose();
_suggestionsTopFocusNode.dispose();
_suggestionsScrollController.dispose();
super.dispose();
}
@override
Widget buildSuggestions(BuildContext context) {
final upQuery = query.trim().toUpperCase();
@ -91,8 +108,12 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
final history = settings.searchHistory.where(notHidden).toList();
return ListView(
controller: _suggestionsScrollController,
padding: const EdgeInsets.only(top: 8),
children: [
TvEdgeFocus(
focusNode: _suggestionsTopFocusNode,
),
_buildFilterRow(
context: context,
filters: [

View file

@ -1,4 +1,5 @@
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
@ -29,8 +30,10 @@ class _DrawerAlbumTabState extends State<DrawerAlbumTab> {
final source = context.read<CollectionSource>();
return Column(
children: [
const DrawerEditorBanner(),
const Divider(height: 0),
if (!settings.useTvLayout) ...[
const DrawerEditorBanner(),
const Divider(height: 0),
],
Flexible(
child: ReorderableListView.builder(
itemBuilder: (context, index) {

View file

@ -1,3 +1,4 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart';
@ -30,8 +31,10 @@ class _DrawerFixedListTabState<T> extends State<DrawerFixedListTab<T>> {
Widget build(BuildContext context) {
return Column(
children: [
const DrawerEditorBanner(),
const Divider(height: 0),
if (!settings.useTvLayout) ...[
const DrawerEditorBanner(),
const Divider(height: 0),
],
Flexible(
child: ReorderableListView.builder(
itemBuilder: (context, index) {

View file

@ -138,10 +138,12 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
child: child!,
);
},
child: InfoPage(
collection: collection,
entryNotifier: widget.entryNotifier,
isScrollingNotifier: _isVerticallyScrollingNotifier,
child: FocusScope(
child: InfoPage(
collection: collection,
entryNotifier: widget.entryNotifier,
isScrollingNotifier: _isVerticallyScrollingNotifier,
),
),
),
);

View file

@ -7,6 +7,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/tv_edge_focus.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart';
@ -281,6 +282,9 @@ class _InfoPageContentState extends State<_InfoPageContent> {
child: CustomScrollView(
controller: widget.scrollController,
slivers: [
const SliverToBoxAdapter(
child: TvEdgeFocus(),
),
InfoAppBar(
entry: entry,
collection: collection,