diff --git a/lib/model/collection_lens.dart b/lib/model/collection_lens.dart index 480d6fc75..666ead83b 100644 --- a/lib/model/collection_lens.dart +++ b/lib/model/collection_lens.dart @@ -10,13 +10,12 @@ import 'package:aves/utils/change_notifier.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; -class CollectionLens with ChangeNotifier { +class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSelectionMixin { final CollectionSource source; final Set filters; GroupFactor groupFactor; SortFactor sortFactor; final AChangeNotifier filterChangeNotifier = AChangeNotifier(); - final AChangeNotifier selectionChangeNotifier = AChangeNotifier(); List _filteredEntries; List _subscriptions = []; @@ -195,9 +194,15 @@ class CollectionLens with ChangeNotifier { _applySort(); _applyGroup(); } +} - // selection +enum SortFactor { date, size, name } +enum GroupFactor { album, month, day } + +enum Activity { browse, select } + +mixin CollectionActivityMixin { final ValueNotifier _activityNotifier = ValueNotifier(Activity.browse); ValueNotifier get activityNotifier => _activityNotifier; @@ -206,17 +211,20 @@ class CollectionLens with ChangeNotifier { bool get isSelecting => _activityNotifier.value == Activity.select; - void browse() { - _activityNotifier.value = Activity.browse; - _clearSelection(); - } + void browse() => _activityNotifier.value = Activity.browse; void select() => _activityNotifier.value = Activity.select; +} + +mixin CollectionSelectionMixin on CollectionActivityMixin { + final AChangeNotifier selectionChangeNotifier = AChangeNotifier(); final Set _selection = {}; Set get selection => _selection; + bool isSelected(List entries) => entries.every(selection.contains); + void addToSelection(List entries) { _selection.addAll(entries); selectionChangeNotifier.notifyListeners(); @@ -227,7 +235,7 @@ class CollectionLens with ChangeNotifier { selectionChangeNotifier.notifyListeners(); } - void _clearSelection() { + void clearSelection() { _selection.clear(); selectionChangeNotifier.notifyListeners(); } @@ -238,9 +246,3 @@ class CollectionLens with ChangeNotifier { selectionChangeNotifier.notifyListeners(); } } - -enum SortFactor { date, size, name } - -enum GroupFactor { album, month, day } - -enum Activity { browse, select } diff --git a/lib/widgets/album/app_bar.dart b/lib/widgets/album/app_bar.dart index abc8c8730..354051221 100644 --- a/lib/widgets/album/app_bar.dart +++ b/lib/widgets/album/app_bar.dart @@ -100,7 +100,10 @@ class _CollectionAppBarState extends State with SingleTickerPr onPressed = Scaffold.of(context).openDrawer; tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip; } else if (collection.isSelecting) { - onPressed = collection.browse; + onPressed = () { + collection.clearSelection(); + collection.browse(); + }; tooltip = MaterialLocalizations.of(context).backButtonTooltip; } return IconButton( @@ -131,9 +134,8 @@ class _CollectionAppBarState extends State with SingleTickerPr return AnimatedBuilder( animation: collection.selectionChangeNotifier, builder: (context, child) { - final selection = collection.selection; - final count = selection.length; - return Text(selection.isEmpty ? 'Select items' : '${count} ${Intl.plural(count, one: 'item', other: 'items')}'); + final count = collection.selection.length; + return Text(Intl.plural(count, zero: 'Select items', one: '${count} item', other: '${count} items')); }, ); } diff --git a/lib/widgets/album/collection_page.dart b/lib/widgets/album/collection_page.dart index 804e9da19..4c756541a 100644 --- a/lib/widgets/album/collection_page.dart +++ b/lib/widgets/album/collection_page.dart @@ -20,6 +20,7 @@ class CollectionPage extends StatelessWidget { body: WillPopScope( onWillPop: () { if (collection.isSelecting) { + collection.clearSelection(); collection.browse(); return SynchronousFuture(false); } diff --git a/lib/widgets/album/grid/header_album.dart b/lib/widgets/album/grid/header_album.dart index 30879da43..d1cb70aaf 100644 --- a/lib/widgets/album/grid/header_album.dart +++ b/lib/widgets/album/grid/header_album.dart @@ -26,6 +26,7 @@ class AlbumSectionHeader extends StatelessWidget { ); } return TitleSectionHeader( + sectionKey: folderPath, leading: albumIcon, title: albumName, trailing: androidFileUtils.isOnSD(folderPath) diff --git a/lib/widgets/album/grid/header_date.dart b/lib/widgets/album/grid/header_date.dart index f52b32d35..278b077dc 100644 --- a/lib/widgets/album/grid/header_date.dart +++ b/lib/widgets/album/grid/header_date.dart @@ -4,10 +4,13 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class DaySectionHeader extends StatelessWidget { + final DateTime date; final String text; - DaySectionHeader({Key key, DateTime date}) - : text = _formatDate(date), + DaySectionHeader({ + Key key, + @required this.date, + }) : text = _formatDate(date), super(key: key); // Examples (en_US): @@ -32,15 +35,21 @@ class DaySectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return TitleSectionHeader(title: text); + return TitleSectionHeader( + sectionKey: date, + title: text, + ); } } class MonthSectionHeader extends StatelessWidget { + final DateTime date; final String text; - MonthSectionHeader({Key key, DateTime date}) - : text = _formatDate(date), + MonthSectionHeader({ + Key key, + @required this.date, + }) : text = _formatDate(date), super(key: key); static DateFormat m = DateFormat.MMMM(); @@ -54,6 +63,9 @@ class MonthSectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return TitleSectionHeader(title: text); + return TitleSectionHeader( + sectionKey: date, + title: text, + ); } } diff --git a/lib/widgets/album/grid/header_generic.dart b/lib/widgets/album/grid/header_generic.dart index 64f25ae0e..c98c53722 100644 --- a/lib/widgets/album/grid/header_generic.dart +++ b/lib/widgets/album/grid/header_generic.dart @@ -7,8 +7,11 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/album/grid/header_album.dart'; import 'package:aves/widgets/album/grid/header_date.dart'; import 'package:aves/widgets/common/fx/outlined_text.dart'; +import 'package:aves/widgets/common/icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; class SectionHeader extends StatelessWidget { final CollectionLens collection; @@ -45,11 +48,7 @@ class SectionHeader extends StatelessWidget { header = _buildAlbumSectionHeader(); break; } - return header != null - ? IgnorePointer( - child: header, - ) - : const SizedBox.shrink(); + return header ?? const SizedBox.shrink(); } Widget _buildAlbumSectionHeader() { @@ -95,13 +94,15 @@ class SectionHeader extends StatelessWidget { } class TitleSectionHeader extends StatelessWidget { + final dynamic sectionKey; final Widget leading, trailing; final String title; const TitleSectionHeader({ Key key, + @required this.sectionKey, this.leading, - this.title, + @required this.title, this.trailing, }) : super(key: key); @@ -117,14 +118,17 @@ class TitleSectionHeader extends StatelessWidget { padding: padding, constraints: const BoxConstraints(minHeight: leadingDimension), child: OutlinedText( - leadingBuilder: leading != null - ? (context, isShadow) => Container( - padding: leadingPadding, - width: leadingDimension, - height: leadingDimension, - child: isShadow ? null : leading, - ) - : null, + leadingBuilder: (context, isShadow) => SectionSelectableLeading( + sectionKey: sectionKey, + browsingBuilder: leading != null + ? (context) => Container( + padding: leadingPadding, + width: leadingDimension, + height: leadingDimension, + child: isShadow ? null : leading, + ) + : null, + ), text: title, trailingBuilder: trailing != null ? (context, isShadow) => Container( @@ -138,3 +142,75 @@ class TitleSectionHeader extends StatelessWidget { ); } } + +class SectionSelectableLeading extends StatelessWidget { + final dynamic sectionKey; + final WidgetBuilder browsingBuilder; + + static final WidgetBuilder _defaultBrowsingBuilder = (context) => const SizedBox.shrink(); + + SectionSelectableLeading({ + Key key, + @required this.sectionKey, + WidgetBuilder browsingBuilder, + }) : browsingBuilder = browsingBuilder ?? _defaultBrowsingBuilder, + super(key: key); + + @override + Widget build(BuildContext context) { + final collection = Provider.of(context); + return ValueListenableBuilder( + valueListenable: collection.activityNotifier, + builder: (context, activity, child) { + final child = collection.isSelecting + ? AnimatedBuilder( + animation: collection.selectionChangeNotifier, + builder: (context, child) { + final sectionEntries = collection.sections[sectionKey]; + final selected = collection.isSelected(sectionEntries); + final child = IconButton( + key: ValueKey(selected), + iconSize: 26, + padding: const EdgeInsets.only(top: 1), + alignment: Alignment.topLeft, + icon: Icon(selected ? AIcons.selected : AIcons.unselected), + onPressed: () { + if (selected) { + collection.removeFromSelection(sectionEntries); + } else { + collection.addToSelection(sectionEntries); + } + }, + tooltip: selected ? 'Deselect section' : 'Select section', + constraints: const BoxConstraints( + minHeight: TitleSectionHeader.leadingDimension, + minWidth: TitleSectionHeader.leadingDimension, + ), + ); + return AnimatedSwitcher( + duration: Duration(milliseconds: (300 * timeDilation).toInt()), + switchInCurve: Curves.easeOutBack, + switchOutCurve: Curves.easeOutBack, + transitionBuilder: (child, animation) => ScaleTransition( + child: child, + scale: animation, + ), + child: child, + ); + }, + ) + : browsingBuilder(context); + return AnimatedSwitcher( + duration: Duration(milliseconds: (300 * timeDilation).toInt()), + switchInCurve: Curves.easeOutBack, + switchOutCurve: Curves.easeOutBack, + transitionBuilder: (child, animation) => ScaleTransition( + child: child, + scale: animation, + ), + child: child, + ); + }, + ); + } +} diff --git a/lib/widgets/album/search/search_delegate.dart b/lib/widgets/album/search/search_delegate.dart index 556c5ef83..b94afe4b6 100644 --- a/lib/widgets/album/search/search_delegate.dart +++ b/lib/widgets/album/search/search_delegate.dart @@ -25,12 +25,12 @@ class ImageSearchDelegate extends SearchDelegate { @override Widget buildLeading(BuildContext context) { return IconButton( - tooltip: 'Back', icon: AnimatedIcon( icon: AnimatedIcons.menu_arrow, progress: transitionAnimation, ), onPressed: () => close(context, null), + tooltip: 'Back', ); } @@ -39,12 +39,12 @@ class ImageSearchDelegate extends SearchDelegate { return [ if (query.isNotEmpty) IconButton( - tooltip: 'Clear', icon: const Icon(OMIcons.clear), onPressed: () { query = ''; showSuggestions(context); }, + tooltip: 'Clear', ), ]; } diff --git a/lib/widgets/album/thumbnail/overlay.dart b/lib/widgets/album/thumbnail/overlay.dart index 0cbf9d006..60776356e 100644 --- a/lib/widgets/album/thumbnail/overlay.dart +++ b/lib/widgets/album/thumbnail/overlay.dart @@ -66,7 +66,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget { ? AnimatedBuilder( animation: collection.selectionChangeNotifier, builder: (context, child) { - final selected = collection.selection.contains(entry); + final selected = collection.isSelected([entry]); final child = OverlayIcon( key: ValueKey(selected), icon: selected ? AIcons.selected : AIcons.unselected,