selection: select/deselect whole section

This commit is contained in:
Thibault Deckers 2020-04-23 11:21:05 +09:00
parent 2f532176ed
commit 1175cff8fe
8 changed files with 135 additions and 41 deletions

View file

@ -10,13 +10,12 @@ import 'package:aves/utils/change_notifier.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class CollectionLens with ChangeNotifier { class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSelectionMixin {
final CollectionSource source; final CollectionSource source;
final Set<CollectionFilter> filters; final Set<CollectionFilter> filters;
GroupFactor groupFactor; GroupFactor groupFactor;
SortFactor sortFactor; SortFactor sortFactor;
final AChangeNotifier filterChangeNotifier = AChangeNotifier(); final AChangeNotifier filterChangeNotifier = AChangeNotifier();
final AChangeNotifier selectionChangeNotifier = AChangeNotifier();
List<ImageEntry> _filteredEntries; List<ImageEntry> _filteredEntries;
List<StreamSubscription> _subscriptions = []; List<StreamSubscription> _subscriptions = [];
@ -195,9 +194,15 @@ class CollectionLens with ChangeNotifier {
_applySort(); _applySort();
_applyGroup(); _applyGroup();
} }
}
// selection enum SortFactor { date, size, name }
enum GroupFactor { album, month, day }
enum Activity { browse, select }
mixin CollectionActivityMixin {
final ValueNotifier<Activity> _activityNotifier = ValueNotifier(Activity.browse); final ValueNotifier<Activity> _activityNotifier = ValueNotifier(Activity.browse);
ValueNotifier<Activity> get activityNotifier => _activityNotifier; ValueNotifier<Activity> get activityNotifier => _activityNotifier;
@ -206,17 +211,20 @@ class CollectionLens with ChangeNotifier {
bool get isSelecting => _activityNotifier.value == Activity.select; bool get isSelecting => _activityNotifier.value == Activity.select;
void browse() { void browse() => _activityNotifier.value = Activity.browse;
_activityNotifier.value = Activity.browse;
_clearSelection();
}
void select() => _activityNotifier.value = Activity.select; void select() => _activityNotifier.value = Activity.select;
}
mixin CollectionSelectionMixin on CollectionActivityMixin {
final AChangeNotifier selectionChangeNotifier = AChangeNotifier();
final Set<ImageEntry> _selection = {}; final Set<ImageEntry> _selection = {};
Set<ImageEntry> get selection => _selection; Set<ImageEntry> get selection => _selection;
bool isSelected(List<ImageEntry> entries) => entries.every(selection.contains);
void addToSelection(List<ImageEntry> entries) { void addToSelection(List<ImageEntry> entries) {
_selection.addAll(entries); _selection.addAll(entries);
selectionChangeNotifier.notifyListeners(); selectionChangeNotifier.notifyListeners();
@ -227,7 +235,7 @@ class CollectionLens with ChangeNotifier {
selectionChangeNotifier.notifyListeners(); selectionChangeNotifier.notifyListeners();
} }
void _clearSelection() { void clearSelection() {
_selection.clear(); _selection.clear();
selectionChangeNotifier.notifyListeners(); selectionChangeNotifier.notifyListeners();
} }
@ -238,9 +246,3 @@ class CollectionLens with ChangeNotifier {
selectionChangeNotifier.notifyListeners(); selectionChangeNotifier.notifyListeners();
} }
} }
enum SortFactor { date, size, name }
enum GroupFactor { album, month, day }
enum Activity { browse, select }

View file

@ -100,7 +100,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
onPressed = Scaffold.of(context).openDrawer; onPressed = Scaffold.of(context).openDrawer;
tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip; tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip;
} else if (collection.isSelecting) { } else if (collection.isSelecting) {
onPressed = collection.browse; onPressed = () {
collection.clearSelection();
collection.browse();
};
tooltip = MaterialLocalizations.of(context).backButtonTooltip; tooltip = MaterialLocalizations.of(context).backButtonTooltip;
} }
return IconButton( return IconButton(
@ -131,9 +134,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
return AnimatedBuilder( return AnimatedBuilder(
animation: collection.selectionChangeNotifier, animation: collection.selectionChangeNotifier,
builder: (context, child) { builder: (context, child) {
final selection = collection.selection; final count = collection.selection.length;
final count = selection.length; return Text(Intl.plural(count, zero: 'Select items', one: '${count} item', other: '${count} items'));
return Text(selection.isEmpty ? 'Select items' : '${count} ${Intl.plural(count, one: 'item', other: 'items')}');
}, },
); );
} }

View file

@ -20,6 +20,7 @@ class CollectionPage extends StatelessWidget {
body: WillPopScope( body: WillPopScope(
onWillPop: () { onWillPop: () {
if (collection.isSelecting) { if (collection.isSelecting) {
collection.clearSelection();
collection.browse(); collection.browse();
return SynchronousFuture(false); return SynchronousFuture(false);
} }

View file

@ -26,6 +26,7 @@ class AlbumSectionHeader extends StatelessWidget {
); );
} }
return TitleSectionHeader( return TitleSectionHeader(
sectionKey: folderPath,
leading: albumIcon, leading: albumIcon,
title: albumName, title: albumName,
trailing: androidFileUtils.isOnSD(folderPath) trailing: androidFileUtils.isOnSD(folderPath)

View file

@ -4,10 +4,13 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class DaySectionHeader extends StatelessWidget { class DaySectionHeader extends StatelessWidget {
final DateTime date;
final String text; final String text;
DaySectionHeader({Key key, DateTime date}) DaySectionHeader({
: text = _formatDate(date), Key key,
@required this.date,
}) : text = _formatDate(date),
super(key: key); super(key: key);
// Examples (en_US): // Examples (en_US):
@ -32,15 +35,21 @@ class DaySectionHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TitleSectionHeader(title: text); return TitleSectionHeader(
sectionKey: date,
title: text,
);
} }
} }
class MonthSectionHeader extends StatelessWidget { class MonthSectionHeader extends StatelessWidget {
final DateTime date;
final String text; final String text;
MonthSectionHeader({Key key, DateTime date}) MonthSectionHeader({
: text = _formatDate(date), Key key,
@required this.date,
}) : text = _formatDate(date),
super(key: key); super(key: key);
static DateFormat m = DateFormat.MMMM(); static DateFormat m = DateFormat.MMMM();
@ -54,6 +63,9 @@ class MonthSectionHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TitleSectionHeader(title: text); return TitleSectionHeader(
sectionKey: date,
title: text,
);
} }
} }

View file

@ -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_album.dart';
import 'package:aves/widgets/album/grid/header_date.dart'; import 'package:aves/widgets/album/grid/header_date.dart';
import 'package:aves/widgets/common/fx/outlined_text.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/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
class SectionHeader extends StatelessWidget { class SectionHeader extends StatelessWidget {
final CollectionLens collection; final CollectionLens collection;
@ -45,11 +48,7 @@ class SectionHeader extends StatelessWidget {
header = _buildAlbumSectionHeader(); header = _buildAlbumSectionHeader();
break; break;
} }
return header != null return header ?? const SizedBox.shrink();
? IgnorePointer(
child: header,
)
: const SizedBox.shrink();
} }
Widget _buildAlbumSectionHeader() { Widget _buildAlbumSectionHeader() {
@ -95,13 +94,15 @@ class SectionHeader extends StatelessWidget {
} }
class TitleSectionHeader extends StatelessWidget { class TitleSectionHeader extends StatelessWidget {
final dynamic sectionKey;
final Widget leading, trailing; final Widget leading, trailing;
final String title; final String title;
const TitleSectionHeader({ const TitleSectionHeader({
Key key, Key key,
@required this.sectionKey,
this.leading, this.leading,
this.title, @required this.title,
this.trailing, this.trailing,
}) : super(key: key); }) : super(key: key);
@ -117,14 +118,17 @@ class TitleSectionHeader extends StatelessWidget {
padding: padding, padding: padding,
constraints: const BoxConstraints(minHeight: leadingDimension), constraints: const BoxConstraints(minHeight: leadingDimension),
child: OutlinedText( child: OutlinedText(
leadingBuilder: leading != null leadingBuilder: (context, isShadow) => SectionSelectableLeading(
? (context, isShadow) => Container( sectionKey: sectionKey,
padding: leadingPadding, browsingBuilder: leading != null
width: leadingDimension, ? (context) => Container(
height: leadingDimension, padding: leadingPadding,
child: isShadow ? null : leading, width: leadingDimension,
) height: leadingDimension,
: null, child: isShadow ? null : leading,
)
: null,
),
text: title, text: title,
trailingBuilder: trailing != null trailingBuilder: trailing != null
? (context, isShadow) => Container( ? (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<CollectionLens>(context);
return ValueListenableBuilder<Activity>(
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,
);
},
);
}
}

View file

@ -25,12 +25,12 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
@override @override
Widget buildLeading(BuildContext context) { Widget buildLeading(BuildContext context) {
return IconButton( return IconButton(
tooltip: 'Back',
icon: AnimatedIcon( icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow, icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation, progress: transitionAnimation,
), ),
onPressed: () => close(context, null), onPressed: () => close(context, null),
tooltip: 'Back',
); );
} }
@ -39,12 +39,12 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
return [ return [
if (query.isNotEmpty) if (query.isNotEmpty)
IconButton( IconButton(
tooltip: 'Clear',
icon: const Icon(OMIcons.clear), icon: const Icon(OMIcons.clear),
onPressed: () { onPressed: () {
query = ''; query = '';
showSuggestions(context); showSuggestions(context);
}, },
tooltip: 'Clear',
), ),
]; ];
} }

View file

@ -66,7 +66,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
? AnimatedBuilder( ? AnimatedBuilder(
animation: collection.selectionChangeNotifier, animation: collection.selectionChangeNotifier,
builder: (context, child) { builder: (context, child) {
final selected = collection.selection.contains(entry); final selected = collection.isSelected([entry]);
final child = OverlayIcon( final child = OverlayIcon(
key: ValueKey(selected), key: ValueKey(selected),
icon: selected ? AIcons.selected : AIcons.unselected, icon: selected ? AIcons.selected : AIcons.unselected,