search: custom app bar
This commit is contained in:
parent
38c0f0897e
commit
6bcb89db85
6 changed files with 219 additions and 42 deletions
|
@ -94,23 +94,21 @@ 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 app setup future complete');
|
||||||
debugPrint('$runtimeType FutureBuilder builder');
|
return _sharedEntry != null
|
||||||
return _sharedEntry != null
|
? SingleFullscreenPage(
|
||||||
? SingleFullscreenPage(
|
entry: _sharedEntry,
|
||||||
entry: _sharedEntry,
|
)
|
||||||
)
|
: MediaStoreCollectionProvider(
|
||||||
: MediaStoreCollectionProvider(
|
child: Consumer<CollectionLens>(
|
||||||
child: Consumer<CollectionLens>(
|
builder: (context, collection, child) => CollectionPage(collection),
|
||||||
builder: (context, collection, child) => CollectionPage(collection),
|
),
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
icon: Icon(OMIcons.search),
|
case PageState.browse:
|
||||||
onPressed: () => showSearch(
|
return IconButton(
|
||||||
context: context,
|
icon: Icon(OMIcons.search),
|
||||||
delegate: ImageSearchDelegate(collection),
|
onPressed: () => stateNotifier.value = PageState.search,
|
||||||
),
|
);
|
||||||
),
|
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 }
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue