diff --git a/lib/main.dart b/lib/main.dart index a6d0eab44..f27e09859 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -94,23 +94,21 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return MediaQueryDataProvider( - child: FutureBuilder( - future: _appSetup, - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.hasError) return const Icon(OMIcons.error); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); - debugPrint('$runtimeType FutureBuilder builder'); - return _sharedEntry != null - ? SingleFullscreenPage( - entry: _sharedEntry, - ) - : MediaStoreCollectionProvider( - child: Consumer( - builder: (context, collection, child) => CollectionPage(collection), - ), - ); - }), - ); + return FutureBuilder( + future: _appSetup, + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasError) return const Icon(OMIcons.error); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + debugPrint('$runtimeType app setup future complete'); + return _sharedEntry != null + ? SingleFullscreenPage( + entry: _sharedEntry, + ) + : MediaStoreCollectionProvider( + child: Consumer( + builder: (context, collection, child) => CollectionPage(collection), + ), + ); + }); } } diff --git a/lib/model/collection_lens.dart b/lib/model/collection_lens.dart index c9c54580a..4fed2a3f5 100644 --- a/lib/model/collection_lens.dart +++ b/lib/model/collection_lens.dart @@ -74,9 +74,19 @@ class CollectionLens with ChangeNotifier { Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}'; + void addFilter(CollectionFilter filter) { + if (filters.contains(filter)) return; + filters.add(filter); + onFilterChanged(); + } + void removeFilter(CollectionFilter filter) { if (!filters.contains(filter)) return; filters.remove(filter); + onFilterChanged(); + } + + void onFilterChanged() { _applyFilters(); _applySort(); _applyGroup(); diff --git a/lib/widgets/album/collection_app_bar.dart b/lib/widgets/album/collection_app_bar.dart index fe77d79ef..869d2005d 100644 --- a/lib/widgets/album/collection_app_bar.dart +++ b/lib/widgets/album/collection_app_bar.dart @@ -1,35 +1,138 @@ import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/filters/query.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/filter_bar.dart'; -import 'package:aves/widgets/album/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 AllCollectionAppBar extends SliverAppBar { - AllCollectionAppBar() - : super( - title: const Text('Aves'), +class CollectionAppBar extends StatefulWidget implements PreferredSizeWidget { + final ValueNotifier stateNotifier; + + @override + final Size preferredSize = Size.fromHeight(kToolbarHeight + FilterBar.preferredHeight); + + CollectionAppBar({this.stateNotifier}); + + @override + _CollectionAppBarState createState() => _CollectionAppBarState(); +} + +class _CollectionAppBarState extends State with SingleTickerProviderStateMixin { + final TextEditingController _searchFieldController = TextEditingController(); + + AnimationController _browseToSearchAnimation; + + ValueNotifier get stateNotifier => widget.stateNotifier; + + @override + void initState() { + super.initState(); + _browseToSearchAnimation = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _registerWidget(widget); + } + + @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) { + stateNotifier.addListener(_onStateChange); + } + + void _unregisterWidget(CollectionAppBar widget) { + stateNotifier.removeListener(_onStateChange); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: stateNotifier, + builder: (context, state, child) { + debugPrint('$runtimeType builder state=$state'); + return SliverAppBar( + leading: _buildAppBarLeading(), + title: _buildAppBarTitle(), actions: _buildActions(), bottom: FilterBar(), floating: true, ); + }, + ); + } - static List _buildActions() { + 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) => Consumer( - builder: (context, collection, child) => IconButton( - icon: Icon(OMIcons.search), - onPressed: () => showSearch( - context: context, - delegate: ImageSearchDelegate(collection), - ), - ), - ), + builder: (context) { + switch (stateNotifier.value) { + case PageState.browse: + return IconButton( + icon: Icon(OMIcons.search), + onPressed: () => stateNotifier.value = PageState.search, + ); + case PageState.search: + return IconButton( + icon: Icon(OMIcons.clear), + onPressed: () => _searchFieldController.clear(), + ); + } + return null; + }, ), Builder( builder: (context) => Consumer( @@ -68,19 +171,19 @@ class AllCollectionAppBar extends SliverAppBar { child: MenuRow(text: 'Stats', icon: OMIcons.pieChart), ), ], - onSelected: (action) => _onActionSelected(context, collection, action), + onSelected: (action) => _onActionSelected(collection, action), ), ), ), ]; } - static void _onActionSelected(BuildContext context, CollectionLens collection, CollectionAction action) async { + void _onActionSelected(CollectionLens collection, CollectionAction action) async { // wait for the popup menu to hide before proceeding with the action await Future.delayed(const Duration(milliseconds: 300)); switch (action) { case CollectionAction.stats: - unawaited(_goToStats(context, collection)); + unawaited(_goToStats(collection)); break; case CollectionAction.groupByAlbum: settings.collectionGroupFactor = GroupFactor.album; @@ -109,7 +212,7 @@ class AllCollectionAppBar extends SliverAppBar { } } - static Future _goToStats(BuildContext context, CollectionLens collection) { + Future _goToStats(CollectionLens collection) { return Navigator.push( context, MaterialPageRoute( @@ -119,6 +222,45 @@ class AllCollectionAppBar extends SliverAppBar { ), ); } + + void _onStateChange() { + if (stateNotifier.value == PageState.search) { + _browseToSearchAnimation.forward(); + } else { + _browseToSearchAnimation.reverse(); + _searchFieldController.clear(); + } + } +} + +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 } diff --git a/lib/widgets/album/collection_page.dart b/lib/widgets/album/collection_page.dart index 7e21ab21c..1b8d61b0e 100644 --- a/lib/widgets/album/collection_page.dart +++ b/lib/widgets/album/collection_page.dart @@ -3,6 +3,7 @@ import 'package:aves/widgets/album/collection_app_bar.dart'; import 'package:aves/widgets/album/collection_drawer.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -13,13 +14,12 @@ class CollectionPage extends StatelessWidget { @override Widget build(BuildContext context) { + debugPrint('$runtimeType build'); return MediaQueryDataProvider( child: ChangeNotifierProvider.value( value: collection, child: Scaffold( - body: ThumbnailCollection( - appBar: AllCollectionAppBar(), - ), + body: CollectionPageBody(), drawer: CollectionDrawer( source: collection.source, ), @@ -29,3 +29,27 @@ class CollectionPage extends StatelessWidget { ); } } + +class CollectionPageBody extends StatelessWidget { + final ValueNotifier _stateNotifier = ValueNotifier(PageState.browse); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () { + if (_stateNotifier.value == PageState.search) { + _stateNotifier.value = PageState.browse; + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: ThumbnailCollection( + appBar: CollectionAppBar( + stateNotifier: _stateNotifier, + ), + ), + ); + } +} + +enum PageState { browse, search } diff --git a/lib/widgets/album/filter_bar.dart b/lib/widgets/album/filter_bar.dart index a3a28fe7e..42ab41f24 100644 --- a/lib/widgets/album/filter_bar.dart +++ b/lib/widgets/album/filter_bar.dart @@ -6,12 +6,13 @@ import 'package:provider/provider.dart'; class FilterBar extends StatelessWidget implements PreferredSizeWidget { static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8); + static final double preferredHeight = kMinInteractiveDimension + padding.vertical; + @override - final Size preferredSize = Size.fromHeight(kMinInteractiveDimension + padding.vertical); + final Size preferredSize = Size.fromHeight(preferredHeight); @override Widget build(BuildContext context) { - debugPrint('$runtimeType build'); final collection = Provider.of(context); final filters = collection.filters.toList()..sort(); diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 85c3d7612..4f38953d3 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -21,6 +21,7 @@ class ThumbnailCollection extends StatelessWidget { @override Widget build(BuildContext context) { + debugPrint('$runtimeType build'); final collection = Provider.of(context); final sections = collection.sections; final sectionKeys = sections.keys.toList(); @@ -45,6 +46,7 @@ class ThumbnailCollection extends StatelessWidget { child: ValueListenableBuilder( valueListenable: _columnCountNotifier, builder: (context, columnCount, child) { + debugPrint('$runtimeType builder columnCount=$columnCount'); final scrollView = CustomScrollView( key: _scrollableKey, primary: true,