From 8a014888a605ff8ed967b76e04c39b0942ea4e14 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 26 Mar 2020 12:36:02 +0900 Subject: [PATCH] added filters to app bar --- lib/model/collection_filters.dart | 94 ++++++++++++++++++++- lib/model/collection_lens.dart | 4 +- lib/widgets/album/collection_page.dart | 2 + lib/widgets/album/filter_bar.dart | 84 ++++++++++++++++++ lib/widgets/album/thumbnail_collection.dart | 5 +- 5 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 lib/widgets/album/filter_bar.dart diff --git a/lib/model/collection_filters.dart b/lib/model/collection_filters.dart index 31d616f9b..ab8c861cd 100644 --- a/lib/model/collection_filters.dart +++ b/lib/model/collection_filters.dart @@ -1,9 +1,16 @@ import 'package:aves/model/image_entry.dart'; +import 'package:flutter/widgets.dart'; +import 'package:outline_material_icons/outline_material_icons.dart'; +import 'package:path/path.dart'; abstract class CollectionFilter { const CollectionFilter(); bool filter(ImageEntry entry); + + String get label; + + IconData get icon => null; } class AlbumFilter extends CollectionFilter { @@ -13,6 +20,21 @@ class AlbumFilter extends CollectionFilter { @override bool filter(ImageEntry entry) => entry.directory == album; + + @override + String get label => album.split(separator).last; + + @override + IconData get icon => OMIcons.photoAlbum; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is AlbumFilter && other.album == album; + } + + @override + int get hashCode => hashValues('AlbumFilter', album); } class TagFilter extends CollectionFilter { @@ -22,6 +44,21 @@ class TagFilter extends CollectionFilter { @override bool filter(ImageEntry entry) => entry.xmpSubjects.contains(tag); + + @override + String get label => tag; + + @override + IconData get icon => OMIcons.localOffer; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is TagFilter && other.tag == tag; + } + + @override + int get hashCode => hashValues('TagFilter', tag); } class CountryFilter extends CollectionFilter { @@ -31,23 +68,74 @@ class CountryFilter extends CollectionFilter { @override bool filter(ImageEntry entry) => entry.isLocated && entry.addressDetails.countryName == country; + + @override + String get label => country; + + @override + IconData get icon => OMIcons.place; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is CountryFilter && other.country == country; + } + + @override + int get hashCode => hashValues('CountryFilter', country); } class VideoFilter extends CollectionFilter { @override bool filter(ImageEntry entry) => entry.isVideo; + + @override + String get label => 'Video'; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is VideoFilter; + } + + @override + int get hashCode => 'VideoFilter'.hashCode; } class GifFilter extends CollectionFilter { @override bool filter(ImageEntry entry) => entry.isGif; + + @override + String get label => 'GIF'; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is GifFilter; + } + + @override + int get hashCode => 'GifFilter'.hashCode; } class MetadataFilter extends CollectionFilter { - final String value; + final String query; - const MetadataFilter(this.value); + const MetadataFilter(this.query); @override - bool filter(ImageEntry entry) => entry.search(value); + bool filter(ImageEntry entry) => entry.search(query); + + @override + String get label => '"${query}"'; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is MetadataFilter && other.query == query; + } + + @override + int get hashCode => hashValues('MetadataFilter', query); } diff --git a/lib/model/collection_lens.dart b/lib/model/collection_lens.dart index 02888c5f6..ec366563e 100644 --- a/lib/model/collection_lens.dart +++ b/lib/model/collection_lens.dart @@ -9,7 +9,7 @@ import 'package:flutter/foundation.dart'; class CollectionLens with ChangeNotifier { final CollectionSource source; - final List filters; + final Set filters; GroupFactor groupFactor; SortFactor sortFactor; @@ -23,7 +23,7 @@ class CollectionLens with ChangeNotifier { List filters, GroupFactor groupFactor, SortFactor sortFactor, - }) : this.filters = [if (filters != null) ...filters.where((f) => f != null)], + }) : this.filters = [if (filters != null) ...filters.where((f) => f != null)].toSet(), this.groupFactor = groupFactor ?? GroupFactor.month, this.sortFactor = sortFactor ?? SortFactor.date { _subscriptions.add(source.eventBus.on().listen((e) => onEntryAdded())); diff --git a/lib/widgets/album/collection_page.dart b/lib/widgets/album/collection_page.dart index 9ad522030..31bd678a0 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/all_collection_app_bar.dart'; import 'package:aves/widgets/album/collection_drawer.dart'; +import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -31,6 +32,7 @@ class CollectionPage extends StatelessWidget { : SliverAppBar( title: Text(title), actions: _buildActions(), + bottom: FilterBar(collection.filters), floating: true, ), ), diff --git a/lib/widgets/album/filter_bar.dart b/lib/widgets/album/filter_bar.dart new file mode 100644 index 000000000..6ff76173c --- /dev/null +++ b/lib/widgets/album/filter_bar.dart @@ -0,0 +1,84 @@ +import 'package:aves/model/collection_filters.dart'; +import 'package:aves/utils/color_utils.dart'; +import 'package:aves/widgets/fullscreen/info/navigation_button.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class FilterBar extends StatelessWidget implements PreferredSizeWidget { + final List filters; + + final ScrollController _scrollController = ScrollController(); + + static const double maxChipWidth = 160; + static const EdgeInsets padding = EdgeInsets.only(left: 8, right: 8, bottom: 8); + + @override + final Size preferredSize = Size.fromHeight(kMinInteractiveDimension + padding.vertical); + + FilterBar(Set filters) + : this.filters = filters.toList() + ..sort((a, b) { + return compareAsciiUpperCase(a.label, b.label); + }); + + @override + Widget build(BuildContext context) { + return Container( + // 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 + // does not misinterpret filter bar scrolling for collection scrolling + onNotification: (notification) => true, + child: ListView.separated( + scrollDirection: Axis.horizontal, + controller: _scrollController, + primary: false, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.all(NavigationButton.buttonBorderWidth / 2), + itemBuilder: (context, index) { + if (index >= filters.length) return null; + final filter = filters[index]; + final icon = filter.icon; + final label = filter.label; + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: maxChipWidth), + child: OutlineButton( + onPressed: () {}, + borderSide: BorderSide( + color: stringToColor(label), + width: NavigationButton.buttonBorderWidth, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(42), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon(icon), + const SizedBox(width: 8), + ], + Flexible( + child: Text( + label, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + ), + ], + ), + ), + ); + }, + separatorBuilder: (context, index) => const SizedBox(width: 8), + itemCount: filters.length, + ), + ), + ); + } +} diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 0db415694..85c3d7612 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -10,7 +10,6 @@ class ThumbnailCollection extends StatelessWidget { final Widget appBar; final WidgetBuilder emptyBuilder; - final ScrollController _scrollController = ScrollController(); final ValueNotifier _columnCountNotifier = ValueNotifier(4); final GlobalKey _scrollableKey = GlobalKey(); @@ -48,7 +47,7 @@ class ThumbnailCollection extends StatelessWidget { builder: (context, columnCount, child) { final scrollView = CustomScrollView( key: _scrollableKey, - controller: _scrollController, + primary: true, slivers: [ if (appBar != null) appBar, if (collection.isEmpty && emptyBuilder != null) @@ -77,7 +76,7 @@ class ThumbnailCollection extends StatelessWidget { heightScrollThumb: avesScrollThumbHeight, backgroundColor: Colors.white, scrollThumbBuilder: avesScrollThumbBuilder(), - controller: _scrollController, + controller: PrimaryScrollController.of(context), padding: EdgeInsets.only( // padding to get scroll thumb below app bar, above nav bar top: topPadding,