diff --git a/lib/main.dart b/lib/main.dart index f27e09859..83cc51cc3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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'; diff --git a/lib/model/collection_lens.dart b/lib/model/collection_lens.dart index 4fed2a3f5..40200b410 100644 --- a/lib/model/collection_lens.dart +++ b/lib/model/collection_lens.dart @@ -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(); } diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 9e31fa23d..72e5fc9e4 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -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; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index 099062f14..aee250563 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -26,6 +26,8 @@ abstract class CollectionFilter implements Comparable { bool filter(ImageEntry entry); + bool get isUnique => true; + String get label; String get tooltip => label; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index e7cfadd2a..b44eb14a9 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -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}'; diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index 7a84c3308..d3d473dcf 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -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; diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 21341ec36..ebe8eecdb 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -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; diff --git a/lib/widgets/album/collection_app_bar.dart b/lib/widgets/album/collection_app_bar.dart index 869d2005d..61f8576c3 100644 --- a/lib/widgets/album/collection_app_bar.dart +++ b/lib/widgets/album/collection_app_bar.dart @@ -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 with SingleTickerPr builder: (context) { switch (stateNotifier.value) { case PageState.browse: - return IconButton( - icon: Icon(OMIcons.search), - onPressed: () => stateNotifier.value = PageState.search, + return Consumer( + 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( diff --git a/lib/widgets/album/collection_drawer.dart b/lib/widgets/album/collection_drawer.dart index 9a9c73436..26e985b5d 100644 --- a/lib/widgets/album/collection_drawer.dart +++ b/lib/widgets/album/collection_drawer.dart @@ -98,7 +98,7 @@ class _CollectionDrawerState extends State { 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, diff --git a/lib/widgets/album/collection_page.dart b/lib/widgets/album/collection_page.dart index 1b8d61b0e..7782098ad 100644 --- a/lib/widgets/album/collection_page.dart +++ b/lib/widgets/album/collection_page.dart @@ -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(), ), ); } diff --git a/lib/widgets/album/empty.dart b/lib/widgets/album/empty.dart new file mode 100644 index 000000000..0f19e03b0 --- /dev/null +++ b/lib/widgets/album/empty.dart @@ -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', + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/album/filter_bar.dart b/lib/widgets/album/filter_bar.dart index 42ab41f24..8f1a1c3c0 100644 --- a/lib/widgets/album/filter_bar.dart +++ b/lib/widgets/album/filter_bar.dart @@ -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( // 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]; diff --git a/lib/widgets/album/search_delegate.dart b/lib/widgets/album/search_delegate.dart index 7475d742f..bb9f3db2f 100644 --- a/lib/widgets/album/search_delegate.dart +++ b/lib/widgets/album/search_delegate.dart @@ -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 { +class ImageSearchDelegate extends SearchDelegate { final CollectionLens collection; ImageSearchDelegate(this.collection); @@ -46,56 +52,70 @@ class ImageSearchDelegate extends SearchDelegate { @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.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 _buildFilterRow({String title, @required Iterable 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(); + } } diff --git a/lib/widgets/album/sections.dart b/lib/widgets/album/sections.dart index 24b7940b1..d90234f88 100644 --- a/lib/widgets/album/sections.dart +++ b/lib/widgets/album/sections.dart @@ -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, ), ); diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index 39f2b9bbe..feabdecc8 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -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(); diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index 4e77e6d01..392df9ddb 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -82,7 +82,7 @@ class InfoPageState extends State { 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 { : SliverList( delegate: SliverChildListDelegate.fixed( [ - BasicSection(entry: entry, onFilter: _goToFilteredCollection), + BasicSection(entry: entry, collection: collection, onFilter: _goToFilteredCollection), locationSection, ], ),