search: custom app bar

This commit is contained in:
Thibault Deckers 2020-03-29 09:49:25 +09:00
parent 38c0f0897e
commit 6bcb89db85
6 changed files with 219 additions and 42 deletions

View file

@ -94,13 +94,12 @@ class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQueryDataProvider( return FutureBuilder(
child: FutureBuilder(
future: _appSetup, future: _appSetup,
builder: (context, AsyncSnapshot<void> snapshot) { builder: (context, AsyncSnapshot<void> snapshot) {
if (snapshot.hasError) return const Icon(OMIcons.error); if (snapshot.hasError) return const Icon(OMIcons.error);
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
debugPrint('$runtimeType FutureBuilder builder'); debugPrint('$runtimeType app setup future complete');
return _sharedEntry != null return _sharedEntry != null
? SingleFullscreenPage( ? SingleFullscreenPage(
entry: _sharedEntry, entry: _sharedEntry,
@ -110,7 +109,6 @@ class _HomePageState extends State<HomePage> {
builder: (context, collection, child) => CollectionPage(collection), builder: (context, collection, child) => CollectionPage(collection),
), ),
); );
}), });
);
} }
} }

View file

@ -74,9 +74,19 @@ class CollectionLens with ChangeNotifier {
Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}'; Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}';
void addFilter(CollectionFilter filter) {
if (filters.contains(filter)) return;
filters.add(filter);
onFilterChanged();
}
void removeFilter(CollectionFilter filter) { void removeFilter(CollectionFilter filter) {
if (!filters.contains(filter)) return; if (!filters.contains(filter)) return;
filters.remove(filter); filters.remove(filter);
onFilterChanged();
}
void onFilterChanged() {
_applyFilters(); _applyFilters();
_applySort(); _applySort();
_applyGroup(); _applyGroup();

View file

@ -1,35 +1,138 @@
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/filters/query.dart';
import 'package:aves/model/settings.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/filter_bar.dart';
import 'package:aves/widgets/album/search_delegate.dart';
import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/common/menu_row.dart';
import 'package:aves/widgets/stats.dart'; import 'package:aves/widgets/stats.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class AllCollectionAppBar extends SliverAppBar { class CollectionAppBar extends StatefulWidget implements PreferredSizeWidget {
AllCollectionAppBar() final ValueNotifier<PageState> stateNotifier;
: super(
title: const Text('Aves'), @override
final Size preferredSize = Size.fromHeight(kToolbarHeight + FilterBar.preferredHeight);
CollectionAppBar({this.stateNotifier});
@override
_CollectionAppBarState createState() => _CollectionAppBarState();
}
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin {
final TextEditingController _searchFieldController = TextEditingController();
AnimationController _browseToSearchAnimation;
ValueNotifier<PageState> 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<PageState>(
valueListenable: stateNotifier,
builder: (context, state, child) {
debugPrint('$runtimeType builder state=$state');
return SliverAppBar(
leading: _buildAppBarLeading(),
title: _buildAppBarTitle(),
actions: _buildActions(), actions: _buildActions(),
bottom: FilterBar(), bottom: FilterBar(),
floating: true, floating: true,
); );
},
);
}
static List<Widget> _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<Widget> _buildActions() {
return [ return [
Builder( Builder(
builder: (context) => Consumer<CollectionLens>( builder: (context) {
builder: (context, collection, child) => IconButton( switch (stateNotifier.value) {
case PageState.browse:
return IconButton(
icon: Icon(OMIcons.search), icon: Icon(OMIcons.search),
onPressed: () => showSearch( onPressed: () => stateNotifier.value = PageState.search,
context: context, );
delegate: ImageSearchDelegate(collection), case PageState.search:
), return IconButton(
), icon: Icon(OMIcons.clear),
), onPressed: () => _searchFieldController.clear(),
);
}
return null;
},
), ),
Builder( Builder(
builder: (context) => Consumer<CollectionLens>( builder: (context) => Consumer<CollectionLens>(
@ -68,19 +171,19 @@ class AllCollectionAppBar extends SliverAppBar {
child: MenuRow(text: 'Stats', icon: OMIcons.pieChart), 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 // wait for the popup menu to hide before proceeding with the action
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
switch (action) { switch (action) {
case CollectionAction.stats: case CollectionAction.stats:
unawaited(_goToStats(context, collection)); unawaited(_goToStats(collection));
break; break;
case CollectionAction.groupByAlbum: case CollectionAction.groupByAlbum:
settings.collectionGroupFactor = GroupFactor.album; settings.collectionGroupFactor = GroupFactor.album;
@ -109,7 +212,7 @@ class AllCollectionAppBar extends SliverAppBar {
} }
} }
static Future _goToStats(BuildContext context, CollectionLens collection) { Future<void> _goToStats(CollectionLens collection) {
return Navigator.push( return Navigator.push(
context, context,
MaterialPageRoute( 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<PageState> stateNotifier;
final TextEditingController controller;
const SearchField({
@required this.stateNotifier,
@required this.controller,
});
@override
Widget build(BuildContext context) {
final collection = Provider.of<CollectionLens>(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 } enum CollectionAction { stats, groupByAlbum, groupByMonth, groupByDay, sortByDate, sortBySize, sortByName }

View file

@ -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/collection_drawer.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -13,13 +14,12 @@ class CollectionPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('$runtimeType build');
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: ChangeNotifierProvider<CollectionLens>.value( child: ChangeNotifierProvider<CollectionLens>.value(
value: collection, value: collection,
child: Scaffold( child: Scaffold(
body: ThumbnailCollection( body: CollectionPageBody(),
appBar: AllCollectionAppBar(),
),
drawer: CollectionDrawer( drawer: CollectionDrawer(
source: collection.source, source: collection.source,
), ),
@ -29,3 +29,27 @@ class CollectionPage extends StatelessWidget {
); );
} }
} }
class CollectionPageBody extends StatelessWidget {
final ValueNotifier<PageState> _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 }

View file

@ -6,12 +6,13 @@ import 'package:provider/provider.dart';
class FilterBar extends StatelessWidget implements PreferredSizeWidget { class FilterBar extends StatelessWidget implements PreferredSizeWidget {
static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8); static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8);
static final double preferredHeight = kMinInteractiveDimension + padding.vertical;
@override @override
final Size preferredSize = Size.fromHeight(kMinInteractiveDimension + padding.vertical); final Size preferredSize = Size.fromHeight(preferredHeight);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('$runtimeType build');
final collection = Provider.of<CollectionLens>(context); final collection = Provider.of<CollectionLens>(context);
final filters = collection.filters.toList()..sort(); final filters = collection.filters.toList()..sort();

View file

@ -21,6 +21,7 @@ class ThumbnailCollection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('$runtimeType build');
final collection = Provider.of<CollectionLens>(context); final collection = Provider.of<CollectionLens>(context);
final sections = collection.sections; final sections = collection.sections;
final sectionKeys = sections.keys.toList(); final sectionKeys = sections.keys.toList();
@ -45,6 +46,7 @@ class ThumbnailCollection extends StatelessWidget {
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: _columnCountNotifier, valueListenable: _columnCountNotifier,
builder: (context, columnCount, child) { builder: (context, columnCount, child) {
debugPrint('$runtimeType builder columnCount=$columnCount');
final scrollView = CustomScrollView( final scrollView = CustomScrollView(
key: _scrollableKey, key: _scrollableKey,
primary: true, primary: true,