search: revert custom app bar, added matching suggestions

This commit is contained in:
Thibault Deckers 2020-03-29 18:07:39 +09:00
parent 6bcb89db85
commit fc014a6274
16 changed files with 159 additions and 90 deletions

View file

@ -5,7 +5,6 @@ import 'package:aves/model/settings.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/viewer_service.dart'; import 'package:aves/utils/viewer_service.dart';
import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -75,13 +75,16 @@ class CollectionLens with ChangeNotifier {
Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}'; Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}';
void addFilter(CollectionFilter filter) { void addFilter(CollectionFilter filter) {
if (filters.contains(filter)) return; if (filter == null || filters.contains(filter)) return;
if (filter.isUnique) {
filters.removeWhere((old) => old.typeKey == filter.typeKey);
}
filters.add(filter); filters.add(filter);
onFilterChanged(); onFilterChanged();
} }
void removeFilter(CollectionFilter filter) { void removeFilter(CollectionFilter filter) {
if (!filters.contains(filter)) return; if (filter == null || !filters.contains(filter)) return;
filters.remove(filter); filters.remove(filter);
onFilterChanged(); onFilterChanged();
} }

View file

@ -16,13 +16,15 @@ class AlbumFilter extends CollectionFilter {
final String album; final String album;
const AlbumFilter(this.album); final String uniqueName;
const AlbumFilter(this.album, this.uniqueName);
@override @override
bool filter(ImageEntry entry) => entry.directory == album; bool filter(ImageEntry entry) => entry.directory == album;
@override @override
String get label => album.split(separator).last; String get label => uniqueName ?? album.split(separator).last;
@override @override
String get tooltip => album; String get tooltip => album;

View file

@ -26,6 +26,8 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
bool filter(ImageEntry entry); bool filter(ImageEntry entry);
bool get isUnique => true;
String get label; String get label;
String get tooltip => label; String get tooltip => label;

View file

@ -1,15 +1,7 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:path/path.dart';
class QueryFilter extends CollectionFilter { class QueryFilter extends CollectionFilter {
static const type = 'query'; static const type = 'query';
@ -21,6 +13,9 @@ class QueryFilter extends CollectionFilter {
@override @override
bool filter(ImageEntry entry) => entry.search(query); bool filter(ImageEntry entry) => entry.search(query);
@override
bool get isUnique => false;
@override @override
String get label => '${query}'; String get label => '${query}';

View file

@ -13,6 +13,9 @@ class TagFilter extends CollectionFilter {
@override @override
bool filter(ImageEntry entry) => entry.xmpSubjects.contains(tag); bool filter(ImageEntry entry) => entry.xmpSubjects.contains(tag);
@override
bool get isUnique => false;
@override @override
String get label => tag; String get label => tag;

View file

@ -6,6 +6,19 @@ class Constants {
// so we give it a `strutStyle` with a slightly larger height // so we give it a `strutStyle` with a slightly larger height
static const overflowStrutStyle = StrutStyle(height: 1.3); static const overflowStrutStyle = StrutStyle(height: 1.3);
static const titleTextStyle = TextStyle(
color: Color(0xFFEEEEEE),
fontSize: 20,
fontFamily: 'Concourse Caps',
shadows: [
Shadow(
offset: Offset(0, 2),
blurRadius: 3,
color: Color(0xFF212121),
),
],
);
// TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling // TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling
static const double thumbnailCacheExtent = 50; static const double thumbnailCacheExtent = 50;

View file

@ -3,6 +3,7 @@ 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/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/foundation.dart';
@ -121,9 +122,17 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
builder: (context) { builder: (context) {
switch (stateNotifier.value) { switch (stateNotifier.value) {
case PageState.browse: case PageState.browse:
return IconButton( return Consumer<CollectionLens>(
builder: (context, collection, child) => IconButton(
icon: Icon(OMIcons.search), icon: Icon(OMIcons.search),
onPressed: () => stateNotifier.value = PageState.search, onPressed: () async {
final filter = await showSearch(
context: context,
delegate: ImageSearchDelegate(collection),
);
collection.addFilter(filter);
},
),
); );
case PageState.search: case PageState.search:
return IconButton( return IconButton(

View file

@ -98,7 +98,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
leading: IconUtils.getAlbumIcon(context: context, album: album), leading: IconUtils.getAlbumIcon(context: context, album: album),
title: CollectionSource.getUniqueAlbumName(album, source.sortedAlbums), title: CollectionSource.getUniqueAlbumName(album, source.sortedAlbums),
dense: true, dense: true,
filter: AlbumFilter(album), filter: AlbumFilter(album, CollectionSource.getUniqueAlbumName(album, source.sortedAlbums)),
); );
final buildTagEntry = (tag) => _FilteredCollectionNavTile( final buildTagEntry = (tag) => _FilteredCollectionNavTile(
source: source, source: source,

View file

@ -1,6 +1,7 @@
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/widgets/album/collection_app_bar.dart'; 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/empty.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/foundation.dart';
@ -47,6 +48,7 @@ class CollectionPageBody extends StatelessWidget {
appBar: CollectionAppBar( appBar: CollectionAppBar(
stateNotifier: _stateNotifier, stateNotifier: _stateNotifier,
), ),
emptyBuilder: (context) => EmptyContent(),
), ),
); );
} }

View file

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
class EmptyContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
const color = Color(0xFF607D8B);
return Align(
alignment: const FractionalOffset(.5, .4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
OMIcons.photo,
size: 64,
color: color,
),
SizedBox(height: 16),
Text(
'Nothing!',
style: TextStyle(
color: color,
fontSize: 22,
fontFamily: 'Concourse',
),
),
],
),
);
}
}

View file

@ -4,9 +4,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; 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 final double preferredHeight = kMinInteractiveDimension;
static final double preferredHeight = kMinInteractiveDimension + padding.vertical;
@override @override
final Size preferredSize = Size.fromHeight(preferredHeight); final Size preferredSize = Size.fromHeight(preferredHeight);
@ -20,7 +18,6 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
// specify transparent as a workaround to prevent // specify transparent as a workaround to prevent
// chip border clipping when the floating app bar is fading // chip border clipping when the floating app bar is fading
color: Colors.transparent, color: Colors.transparent,
padding: padding,
height: preferredSize.height, height: preferredSize.height,
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
// cancel notification bubbling so that the draggable scrollbar // cancel notification bubbling so that the draggable scrollbar
@ -28,9 +25,8 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
onNotification: (notification) => true, onNotification: (notification) => true,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
primary: false,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(AvesFilterChip.buttonBorderWidth / 2), padding: const EdgeInsets.all(AvesFilterChip.buttonBorderWidth / 2) + const EdgeInsets.symmetric(horizontal: 6),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= filters.length) return null; if (index >= filters.length) return null;
final filter = filters[index]; final filter = filters[index];

View file

@ -1,13 +1,19 @@
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/collection_source.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/country.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/gif.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/filters/tag.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/model/filters/video.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/aves_filter_chip.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:provider/provider.dart';
class ImageSearchDelegate extends SearchDelegate<ImageEntry> { class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
final CollectionLens collection; final CollectionLens collection;
ImageSearchDelegate(this.collection); ImageSearchDelegate(this.collection);
@ -46,56 +52,70 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
@override @override
Widget buildSuggestions(BuildContext context) { Widget buildSuggestions(BuildContext context) {
return const SizedBox.shrink(); final source = collection.source;
} final upQuery = query.toUpperCase();
final containQuery = (String s) => s.toUpperCase().contains(upQuery);
@override return SafeArea(
Widget buildResults(BuildContext context) { child: ListView(
if (query.isEmpty) { children: [
showSuggestions(context); ..._buildFilterRow(
return const SizedBox.shrink(); filters: [FavouriteFilter(), VideoFilter(), GifFilter()].where((f) => containQuery(f.label)),
}
return MediaQueryDataProvider(
child: ChangeNotifierProvider<CollectionLens>.value(
value: CollectionLens(
source: collection.source,
filters: [QueryFilter(query.toLowerCase())],
groupFactor: collection.groupFactor,
sortFactor: collection.sortFactor,
), ),
child: ThumbnailCollection( ..._buildFilterRow(
emptyBuilder: (context) => _EmptyContent(), title: 'Countries',
filters: source.sortedCountries.where(containQuery).map((s) => CountryFilter(s)),
), ),
..._buildFilterRow(
title: 'Albums',
filters: source.sortedAlbums.where(containQuery).map((s) => AlbumFilter(s, CollectionSource.getUniqueAlbumName(s, source.sortedAlbums))).where((f) => containQuery(f.uniqueName)),
), ),
); ..._buildFilterRow(
} title: 'Tags',
} filters: source.sortedTags.where(containQuery).map((s) => TagFilter(s)),
class _EmptyContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
const color = Color(0xFF607D8B);
return Align(
alignment: const FractionalOffset(.5, .4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
OMIcons.photo,
size: 64,
color: color,
),
SizedBox(height: 16),
Text(
'No match',
style: TextStyle(
color: color,
fontSize: 22,
fontFamily: 'Concourse',
),
), ),
], ],
), ),
); );
} }
List<Widget> _buildFilterRow({String title, @required Iterable<CollectionFilter> filters}) {
if (filters.isEmpty) return [];
final filtersList = filters.toList();
return [
if (title != null && title.isNotEmpty)
Padding(
padding: const EdgeInsets.all(16),
child: Text(
title,
style: Constants.titleTextStyle,
),
),
Container(
height: kMinInteractiveDimension,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(AvesFilterChip.buttonBorderWidth / 2) + const EdgeInsets.symmetric(horizontal: 6),
itemBuilder: (context, index) {
if (index >= filtersList.length) return null;
final filter = filtersList[index];
return Center(
child: AvesFilterChip(
filter: filter,
onPressed: (filter) => close(context, filter),
),
);
},
separatorBuilder: (context, index) => const SizedBox(width: 8),
itemCount: filtersList.length,
),
),
];
}
@override
Widget buildResults(BuildContext context) {
close(context, QueryFilter(query));
return const SizedBox.shrink();
}
} }

View file

@ -1,3 +1,4 @@
import 'package:aves/utils/constants.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/fx/outlined_text.dart'; import 'package:aves/widgets/common/fx/outlined_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -56,18 +57,6 @@ class TitleSectionHeader extends StatelessWidget {
static const leadingDimension = 32.0; static const leadingDimension = 32.0;
static const leadingPadding = EdgeInsets.only(right: 8, bottom: 4); static const leadingPadding = EdgeInsets.only(right: 8, bottom: 4);
static const textStyle = TextStyle(
color: Color(0xFFEEEEEE),
fontSize: 20,
fontFamily: 'Concourse Caps',
shadows: [
Shadow(
offset: Offset(0, 2),
blurRadius: 3,
color: Color(0xFF212121),
),
],
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -83,7 +72,7 @@ class TitleSectionHeader extends StatelessWidget {
) )
: null, : null,
text: title, text: title,
style: textStyle, style: Constants.titleTextStyle,
outlineWidth: 2, outlineWidth: 2,
), ),
); );

View file

@ -1,3 +1,5 @@
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/collection_source.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/gif.dart'; import 'package:aves/model/filters/gif.dart';
@ -13,11 +15,13 @@ import 'package:intl/intl.dart';
class BasicSection extends StatelessWidget { class BasicSection extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
final CollectionLens collection;
final FilterCallback onFilter; final FilterCallback onFilter;
const BasicSection({ const BasicSection({
Key key, Key key,
@required this.entry, @required this.entry,
this.collection,
@required this.onFilter, @required this.onFilter,
}) : super(key: key); }) : super(key: key);
@ -44,11 +48,12 @@ class BasicSection extends StatelessWidget {
ValueListenableBuilder( ValueListenableBuilder(
valueListenable: entry.isFavouriteNotifier, valueListenable: entry.isFavouriteNotifier,
builder: (context, isFavourite, child) { builder: (context, isFavourite, child) {
final album = entry.directory;
final filters = [ final filters = [
if (entry.isVideo) VideoFilter(), if (entry.isVideo) VideoFilter(),
if (entry.isGif) GifFilter(), if (entry.isGif) GifFilter(),
if (isFavourite) FavouriteFilter(), if (isFavourite) FavouriteFilter(),
if (entry.directory != null) AlbumFilter(entry.directory), if (album != null) AlbumFilter(album, CollectionSource.getUniqueAlbumName(album, collection?.source?.sortedAlbums)),
...tags.map((tag) => TagFilter(tag)), ...tags.map((tag) => TagFilter(tag)),
]..sort(); ]..sort();
if (filters.isEmpty) return const SizedBox.shrink(); if (filters.isEmpty) return const SizedBox.shrink();

View file

@ -82,7 +82,7 @@ class InfoPageState extends State<InfoPage> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded(child: BasicSection(entry: entry, onFilter: _goToFilteredCollection)), Expanded(child: BasicSection(entry: entry, collection: collection, onFilter: _goToFilteredCollection)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: locationSection), Expanded(child: locationSection),
], ],
@ -91,7 +91,7 @@ class InfoPageState extends State<InfoPage> {
: SliverList( : SliverList(
delegate: SliverChildListDelegate.fixed( delegate: SliverChildListDelegate.fixed(
[ [
BasicSection(entry: entry, onFilter: _goToFilteredCollection), BasicSection(entry: entry, collection: collection, onFilter: _goToFilteredCollection),
locationSection, locationSection,
], ],
), ),