selection: select/deselect whole section
This commit is contained in:
parent
2f532176ed
commit
1175cff8fe
8 changed files with 135 additions and 41 deletions
|
@ -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 }
|
||||
|
|
|
@ -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'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ class CollectionPage extends StatelessWidget {
|
|||
body: WillPopScope(
|
||||
onWillPop: () {
|
||||
if (collection.isSelecting) {
|
||||
collection.clearSelection();
|
||||
collection.browse();
|
||||
return SynchronousFuture(false);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ class AlbumSectionHeader extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
return TitleSectionHeader(
|
||||
sectionKey: folderPath,
|
||||
leading: albumIcon,
|
||||
title: albumName,
|
||||
trailing: androidFileUtils.isOnSD(folderPath)
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue