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: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 }
|
|
||||||
|
|
|
@ -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')}');
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
browsingBuilder: leading != null
|
||||||
|
? (context) => Container(
|
||||||
padding: leadingPadding,
|
padding: leadingPadding,
|
||||||
width: leadingDimension,
|
width: leadingDimension,
|
||||||
height: leadingDimension,
|
height: leadingDimension,
|
||||||
child: isShadow ? null : leading,
|
child: isShadow ? null : leading,
|
||||||
)
|
)
|
||||||
: null,
|
: 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue