aves/lib/widgets/common/search/page.dart
2024-02-15 23:23:05 +01:00

164 lines
5.1 KiB
Dart

import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/search/delegate.dart';
import 'package:aves/widgets/common/search/route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class SearchPage extends StatefulWidget {
static const routeName = '/search';
final AvesSearchDelegate delegate;
final Animation<double> animation;
const SearchPage({
super.key,
required this.delegate,
required this.animation,
});
@override
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
final Debouncer _debouncer = Debouncer(delay: ADurations.searchDebounceDelay);
final FocusNode _searchFieldFocusNode = FocusNode();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void initState() {
super.initState();
_registerWidget(widget);
widget.animation.addStatusListener(_onAnimationStatusChanged);
_searchFieldFocusNode.addListener(_onFocusChanged);
}
@override
void didUpdateWidget(covariant SearchPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.delegate != widget.delegate) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
}
@override
void dispose() {
_unregisterWidget(widget);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
_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.searchFieldFocusNode = _searchFieldFocusNode;
}
void _unregisterWidget(SearchPage widget) {
widget.delegate.queryTextController.removeListener(_onQueryChanged);
widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate.searchFieldFocusNode = null;
}
void _onAnimationStatusChanged(AnimationStatus status) {
if (status != AnimationStatus.completed) {
return;
}
widget.animation.removeStatusListener(_onAnimationStatusChanged);
Future.delayed(ADurations.pageTransitionAnimation * timeDilation).then((_) {
if (!mounted) return;
_searchFieldFocusNode.requestFocus();
});
}
void _onFocusChanged() {
if (_searchFieldFocusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) {
widget.delegate.showSuggestions(context);
}
}
void _onQueryChanged() {
_debouncer(() {
if (!mounted) return;
// rebuild ourselves because query changed.
setState(() {});
});
}
void _onSearchBodyChanged() {
setState(() {
// rebuild ourselves because search body changed.
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Widget? body;
switch (widget.delegate.currentBody) {
case SearchBody.suggestions:
body = KeyedSubtree(
key: const ValueKey<SearchBody>(SearchBody.suggestions),
child: widget.delegate.buildSuggestions(context),
);
case SearchBody.results:
body = KeyedSubtree(
key: const ValueKey<SearchBody>(SearchBody.results),
child: widget.delegate.buildResults(context),
);
case null:
break;
}
return AvesScaffold(
appBar: AppBar(
leading: Hero(
tag: AvesAppBar.leadingHeroTag,
transitionOnUserGestures: true,
child: Center(child: widget.delegate.buildLeading(context)),
),
title: Hero(
tag: AvesAppBar.titleHeroTag,
transitionOnUserGestures: true,
child: DefaultTextStyle.merge(
style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]),
child: TextField(
controller: widget.delegate.queryTextController,
focusNode: _searchFieldFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.delegate.searchFieldLabel,
hintStyle: theme.inputDecorationTheme.hintStyle,
),
textInputAction: TextInputAction.search,
style: Themes.searchFieldStyle(context),
onSubmitted: (_) => widget.delegate.showResults(context),
),
),
),
actions: widget.delegate.buildActions(context),
),
body: AvesPopScope(
handlers: [
TvNavigationPopHandler.pop,
_doubleBackPopHandler.pop,
],
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: body,
),
),
);
}
}