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/viewer_service.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/fullscreen/fullscreen_page.dart';
import 'package:flutter/material.dart';

View file

@ -75,13 +75,16 @@ class CollectionLens with ChangeNotifier {
Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}';
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);
onFilterChanged();
}
void removeFilter(CollectionFilter filter) {
if (!filters.contains(filter)) return;
if (filter == null || !filters.contains(filter)) return;
filters.remove(filter);
onFilterChanged();
}

View file

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

View file

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

View file

@ -1,15 +1,7 @@
import 'package:aves/model/filters/filters.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:outline_material_icons/outline_material_icons.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:path/path.dart';
class QueryFilter extends CollectionFilter {
static const type = 'query';
@ -21,6 +13,9 @@ class QueryFilter extends CollectionFilter {
@override
bool filter(ImageEntry entry) => entry.search(query);
@override
bool get isUnique => false;
@override
String get label => '${query}';

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import 'package:aves/model/collection_lens.dart';
import 'package:aves/widgets/album/collection_app_bar.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/common/data_providers/media_query_data_provider.dart';
import 'package:flutter/foundation.dart';
@ -47,6 +48,7 @@ class CollectionPageBody extends StatelessWidget {
appBar: CollectionAppBar(
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';
class FilterBar extends StatelessWidget implements PreferredSizeWidget {
static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8);
static final double preferredHeight = kMinInteractiveDimension + padding.vertical;
static final double preferredHeight = kMinInteractiveDimension;
@override
final Size preferredSize = Size.fromHeight(preferredHeight);
@ -20,7 +18,6 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
// specify transparent as a workaround to prevent
// chip border clipping when the floating app bar is fading
color: Colors.transparent,
padding: padding,
height: preferredSize.height,
child: NotificationListener<ScrollNotification>(
// cancel notification bubbling so that the draggable scrollbar
@ -28,9 +25,8 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
onNotification: (notification) => true,
child: ListView.separated(
scrollDirection: Axis.horizontal,
primary: false,
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) {
if (index >= filters.length) return null;
final filter = filters[index];

View file

@ -1,13 +1,19 @@
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/image_entry.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/filters/video.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:flutter/material.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;
ImageSearchDelegate(this.collection);
@ -46,56 +52,70 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
@override
Widget buildSuggestions(BuildContext context) {
return const SizedBox.shrink();
}
@override
Widget buildResults(BuildContext context) {
if (query.isEmpty) {
showSuggestions(context);
return const SizedBox.shrink();
}
return MediaQueryDataProvider(
child: ChangeNotifierProvider<CollectionLens>.value(
value: CollectionLens(
source: collection.source,
filters: [QueryFilter(query.toLowerCase())],
groupFactor: collection.groupFactor,
sortFactor: collection.sortFactor,
),
child: ThumbnailCollection(
emptyBuilder: (context) => _EmptyContent(),
),
),
);
}
}
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,
final source = collection.source;
final upQuery = query.toUpperCase();
final containQuery = (String s) => s.toUpperCase().contains(upQuery);
return SafeArea(
child: ListView(
children: [
..._buildFilterRow(
filters: [FavouriteFilter(), VideoFilter(), GifFilter()].where((f) => containQuery(f.label)),
),
SizedBox(height: 16),
Text(
'No match',
style: TextStyle(
color: color,
fontSize: 22,
fontFamily: 'Concourse',
),
..._buildFilterRow(
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)),
),
],
),
);
}
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/widgets/common/fx/outlined_text.dart';
import 'package:flutter/material.dart';
@ -56,18 +57,6 @@ class TitleSectionHeader extends StatelessWidget {
static const leadingDimension = 32.0;
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
Widget build(BuildContext context) {
@ -83,7 +72,7 @@ class TitleSectionHeader extends StatelessWidget {
)
: null,
text: title,
style: textStyle,
style: Constants.titleTextStyle,
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/favourite.dart';
import 'package:aves/model/filters/gif.dart';
@ -13,11 +15,13 @@ import 'package:intl/intl.dart';
class BasicSection extends StatelessWidget {
final ImageEntry entry;
final CollectionLens collection;
final FilterCallback onFilter;
const BasicSection({
Key key,
@required this.entry,
this.collection,
@required this.onFilter,
}) : super(key: key);
@ -44,11 +48,12 @@ class BasicSection extends StatelessWidget {
ValueListenableBuilder(
valueListenable: entry.isFavouriteNotifier,
builder: (context, isFavourite, child) {
final album = entry.directory;
final filters = [
if (entry.isVideo) VideoFilter(),
if (entry.isGif) GifFilter(),
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)),
]..sort();
if (filters.isEmpty) return const SizedBox.shrink();

View file

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