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:flutter/foundation.dart';
class CollectionLens with ChangeNotifier {
class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSelectionMixin {
final CollectionSource source;
final Set<CollectionFilter> filters;
GroupFactor groupFactor;
SortFactor sortFactor;
final AChangeNotifier filterChangeNotifier = AChangeNotifier();
final AChangeNotifier selectionChangeNotifier = AChangeNotifier();
List<ImageEntry> _filteredEntries;
List<StreamSubscription> _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<Activity> _activityNotifier = ValueNotifier(Activity.browse);
ValueNotifier<Activity> 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<ImageEntry> _selection = {};
Set<ImageEntry> get selection => _selection;
bool isSelected(List<ImageEntry> entries) => entries.every(selection.contains);
void addToSelection(List<ImageEntry> 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 }

View file

@ -100,7 +100,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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<CollectionAppBar> 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'));
},
);
}

View file

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

View file

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

View file

@ -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,
);
}
}

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_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<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
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<CollectionFilter> {
return [
if (query.isNotEmpty)
IconButton(
tooltip: 'Clear',
icon: const Icon(OMIcons.clear),
onPressed: () {
query = '';
showSuggestions(context);
},
tooltip: 'Clear',
),
];
}

View file

@ -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,