import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/search/search_delegate.dart'; import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/stats.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:pedantic/pedantic.dart'; import 'package:provider/provider.dart'; class CollectionAppBar extends StatefulWidget { final ValueNotifier stateNotifier; final ValueNotifier appBarHeightNotifier; final CollectionLens collection; const CollectionAppBar({ Key key, @required this.stateNotifier, @required this.appBarHeightNotifier, @required this.collection, }) : super(key: key); @override _CollectionAppBarState createState() => _CollectionAppBarState(); } class _CollectionAppBarState extends State with SingleTickerProviderStateMixin { final TextEditingController _searchFieldController = TextEditingController(); AnimationController _browseToSearchAnimation; ValueNotifier get stateNotifier => widget.stateNotifier; CollectionLens get collection => widget.collection; bool get hasFilters => collection.filters.isNotEmpty; @override void initState() { super.initState(); _browseToSearchAnimation = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _registerWidget(widget); WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight()); } @override void didUpdateWidget(CollectionAppBar oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); } @override void dispose() { _unregisterWidget(widget); _browseToSearchAnimation.dispose(); super.dispose(); } void _registerWidget(CollectionAppBar widget) { widget.stateNotifier.addListener(_onStateChange); widget.collection.filterChangeNotifier.addListener(_updateHeight); } void _unregisterWidget(CollectionAppBar widget) { widget.stateNotifier.removeListener(_onStateChange); widget.collection.filterChangeNotifier.removeListener(_updateHeight); } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: stateNotifier, builder: (context, state, child) { debugPrint('$runtimeType builder state=$state'); return AnimatedBuilder( animation: collection.filterChangeNotifier, builder: (context, child) => SliverAppBar( leading: _buildAppBarLeading(), title: _buildAppBarTitle(), actions: _buildActions(), bottom: hasFilters ? FilterBar() : null, floating: true, ), ); }, ); } Widget _buildAppBarLeading() { VoidCallback onPressed; String tooltip; switch (stateNotifier.value) { case PageState.browse: onPressed = () => Scaffold.of(context).openDrawer(); tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip; break; case PageState.search: onPressed = () => stateNotifier.value = PageState.browse; tooltip = MaterialLocalizations.of(context).backButtonTooltip; break; } return IconButton( icon: AnimatedIcon( icon: AnimatedIcons.menu_arrow, progress: _browseToSearchAnimation, ), onPressed: onPressed, tooltip: tooltip, ); } Widget _buildAppBarTitle() { switch (stateNotifier.value) { case PageState.browse: return const Text('Aves'); case PageState.search: return SearchField( stateNotifier: stateNotifier, controller: _searchFieldController, ); } return null; } List _buildActions() { return [ Builder( builder: (context) { switch (stateNotifier.value) { case PageState.browse: return IconButton( icon: Icon(OMIcons.search), onPressed: () async { final filter = await showSearch( context: context, delegate: ImageSearchDelegate(collection), ); collection.addFilter(filter); }, ); case PageState.search: return IconButton( icon: Icon(OMIcons.clear), onPressed: () => _searchFieldController.clear(), ); } return null; }, ), Builder( builder: (context) => PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( value: CollectionAction.sortByDate, child: MenuRow(text: 'Sort by date', checked: collection.sortFactor == SortFactor.date), ), PopupMenuItem( value: CollectionAction.sortBySize, child: MenuRow(text: 'Sort by size', checked: collection.sortFactor == SortFactor.size), ), PopupMenuItem( value: CollectionAction.sortByName, child: MenuRow(text: 'Sort by name', checked: collection.sortFactor == SortFactor.name), ), const PopupMenuDivider(), if (collection.sortFactor == SortFactor.date) ...[ PopupMenuItem( value: CollectionAction.groupByAlbum, child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album), ), PopupMenuItem( value: CollectionAction.groupByMonth, child: MenuRow(text: 'Group by month', checked: collection.groupFactor == GroupFactor.month), ), PopupMenuItem( value: CollectionAction.groupByDay, child: MenuRow(text: 'Group by day', checked: collection.groupFactor == GroupFactor.day), ), const PopupMenuDivider(), ], PopupMenuItem( value: CollectionAction.stats, child: MenuRow(text: 'Stats', icon: OMIcons.pieChart), ), ], onSelected: _onActionSelected, ), ), ]; } void _onActionSelected(CollectionAction action) async { // wait for the popup menu to hide before proceeding with the action await Future.delayed(Constants.popupMenuTransitionDuration); switch (action) { case CollectionAction.stats: unawaited(_goToStats()); break; case CollectionAction.groupByAlbum: settings.collectionGroupFactor = GroupFactor.album; collection.group(GroupFactor.album); break; case CollectionAction.groupByMonth: settings.collectionGroupFactor = GroupFactor.month; collection.group(GroupFactor.month); break; case CollectionAction.groupByDay: settings.collectionGroupFactor = GroupFactor.day; collection.group(GroupFactor.day); break; case CollectionAction.sortByDate: settings.collectionSortFactor = SortFactor.date; collection.sort(SortFactor.date); break; case CollectionAction.sortBySize: settings.collectionSortFactor = SortFactor.size; collection.sort(SortFactor.size); break; case CollectionAction.sortByName: settings.collectionSortFactor = SortFactor.name; collection.sort(SortFactor.name); break; } } Future _goToStats() { return Navigator.push( context, MaterialPageRoute( builder: (context) => StatsPage( collection: collection, ), ), ); } void _onStateChange() { if (stateNotifier.value == PageState.search) { _browseToSearchAnimation.forward(); } else { _browseToSearchAnimation.reverse(); _searchFieldController.clear(); } } void _updateHeight() { widget.appBarHeightNotifier.value = kToolbarHeight + (hasFilters ? FilterBar.preferredHeight : 0); } } class SearchField extends StatelessWidget { final ValueNotifier stateNotifier; final TextEditingController controller; const SearchField({ @required this.stateNotifier, @required this.controller, }); @override Widget build(BuildContext context) { final collection = Provider.of(context); return TextField( controller: controller, decoration: const InputDecoration( hintText: 'Search...', border: InputBorder.none, ), autofocus: true, onSubmitted: (query) { query = query.trim(); if (query.isNotEmpty) { collection.addFilter(QueryFilter(query)); } stateNotifier.value = PageState.browse; }, ); } } enum CollectionAction { stats, groupByAlbum, groupByMonth, groupByDay, sortByDate, sortBySize, sortByName }