multi selection generalization prep
This commit is contained in:
parent
981fd41e27
commit
81ab903d39
13 changed files with 303 additions and 268 deletions
45
lib/model/selection.dart
Normal file
45
lib/model/selection.dart
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class Selection<T> extends ChangeNotifier {
|
||||||
|
bool _isSelecting = false;
|
||||||
|
|
||||||
|
bool get isSelecting => _isSelecting;
|
||||||
|
|
||||||
|
final Set<T> _selection = {};
|
||||||
|
|
||||||
|
Set<T> get selection => _selection;
|
||||||
|
|
||||||
|
void browse() {
|
||||||
|
clearSelection();
|
||||||
|
_isSelecting = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void select() {
|
||||||
|
_isSelecting = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSelected(Iterable<T> items) => items.every(selection.contains);
|
||||||
|
|
||||||
|
void addToSelection(Iterable<T> items) {
|
||||||
|
_selection.addAll(items);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeFromSelection(Iterable<T> items) {
|
||||||
|
_selection.removeAll(items);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearSelection() {
|
||||||
|
_selection.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleSelection(T item) {
|
||||||
|
if (_selection.isEmpty) select();
|
||||||
|
if (!_selection.remove(item)) _selection.add(item);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'enums.dart';
|
import 'enums.dart';
|
||||||
|
|
||||||
class CollectionLens with ChangeNotifier, CollectionActivityMixin {
|
class CollectionLens with ChangeNotifier {
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
final Set<CollectionFilter> filters;
|
final Set<CollectionFilter> filters;
|
||||||
EntryGroupFactor groupFactor;
|
EntryGroupFactor groupFactor;
|
||||||
|
@ -213,55 +213,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin {
|
||||||
_sortedEntries?.removeWhere(entries.contains);
|
_sortedEntries?.removeWhere(entries.contains);
|
||||||
sections.forEach((key, sectionEntries) => sectionEntries.removeWhere(entries.contains));
|
sections.forEach((key, sectionEntries) => sectionEntries.removeWhere(entries.contains));
|
||||||
sections = Map.unmodifiable(Map.fromEntries(sections.entries.where((kv) => kv.value.isNotEmpty)));
|
sections = Map.unmodifiable(Map.fromEntries(sections.entries.where((kv) => kv.value.isNotEmpty)));
|
||||||
selection.removeAll(entries);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin CollectionActivityMixin {
|
|
||||||
final ValueNotifier<Activity> _activityNotifier = ValueNotifier(Activity.browse);
|
|
||||||
|
|
||||||
ValueNotifier<Activity> get activityNotifier => _activityNotifier;
|
|
||||||
|
|
||||||
bool get isBrowsing => _activityNotifier.value == Activity.browse;
|
|
||||||
|
|
||||||
bool get isSelecting => _activityNotifier.value == Activity.select;
|
|
||||||
|
|
||||||
void browse() {
|
|
||||||
clearSelection();
|
|
||||||
_activityNotifier.value = Activity.browse;
|
|
||||||
}
|
|
||||||
|
|
||||||
void select() => _activityNotifier.value = Activity.select;
|
|
||||||
|
|
||||||
// selection
|
|
||||||
|
|
||||||
final AChangeNotifier selectionChangeNotifier = AChangeNotifier();
|
|
||||||
|
|
||||||
final Set<AvesEntry> _selection = {};
|
|
||||||
|
|
||||||
Set<AvesEntry> get selection => _selection;
|
|
||||||
|
|
||||||
bool isSelected(Iterable<AvesEntry> entries) => entries.every(selection.contains);
|
|
||||||
|
|
||||||
void addToSelection(Iterable<AvesEntry> entries) {
|
|
||||||
_selection.addAll(entries);
|
|
||||||
selectionChangeNotifier.notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeFromSelection(Iterable<AvesEntry> entries) {
|
|
||||||
_selection.removeAll(entries);
|
|
||||||
selectionChangeNotifier.notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearSelection() {
|
|
||||||
_selection.clear();
|
|
||||||
selectionChangeNotifier.notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleSelection(AvesEntry entry) {
|
|
||||||
if (_selection.isEmpty) select();
|
|
||||||
if (!_selection.remove(entry)) _selection.add(entry);
|
|
||||||
selectionChangeNotifier.notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
enum Activity { browse, select }
|
|
||||||
|
|
||||||
enum SourceState { loading, cataloguing, locating, ready }
|
enum SourceState { loading, cataloguing, locating, ready }
|
||||||
|
|
||||||
enum ChipSortFactor { date, name, count }
|
enum ChipSortFactor { date, name, count }
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/actions/collection_actions.dart';
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
@ -49,6 +50,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
|
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
|
||||||
late AnimationController _browseToSelectAnimation;
|
late AnimationController _browseToSelectAnimation;
|
||||||
late Future<bool> _canAddShortcutsLoader;
|
late Future<bool> _canAddShortcutsLoader;
|
||||||
|
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
|
||||||
|
@ -63,6 +65,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
duration: Durations.iconAnimation,
|
duration: Durations.iconAnimation,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
_isSelectingNotifier.addListener(_onActivityChange);
|
||||||
_canAddShortcutsLoader = AppShortcutService.canPin();
|
_canAddShortcutsLoader = AppShortcutService.canPin();
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
WidgetsBinding.instance!.addPostFrameCallback((_) => _updateHeight());
|
WidgetsBinding.instance!.addPostFrameCallback((_) => _updateHeight());
|
||||||
|
@ -78,35 +81,35 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
|
_isSelectingNotifier.removeListener(_onActivityChange);
|
||||||
_browseToSelectAnimation.dispose();
|
_browseToSelectAnimation.dispose();
|
||||||
_searchFieldController.dispose();
|
_searchFieldController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerWidget(CollectionAppBar widget) {
|
void _registerWidget(CollectionAppBar widget) {
|
||||||
widget.collection.activityNotifier.addListener(_onActivityChange);
|
|
||||||
widget.collection.filterChangeNotifier.addListener(_updateHeight);
|
widget.collection.filterChangeNotifier.addListener(_updateHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unregisterWidget(CollectionAppBar widget) {
|
void _unregisterWidget(CollectionAppBar widget) {
|
||||||
widget.collection.activityNotifier.removeListener(_onActivityChange);
|
|
||||||
widget.collection.filterChangeNotifier.removeListener(_updateHeight);
|
widget.collection.filterChangeNotifier.removeListener(_updateHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
return ValueListenableBuilder<Activity>(
|
return Selector<Selection<AvesEntry>, bool>(
|
||||||
valueListenable: collection.activityNotifier,
|
selector: (context, selection) => selection.isSelecting,
|
||||||
builder: (context, activity, child) {
|
builder: (context, isSelecting, child) {
|
||||||
|
_isSelectingNotifier.value = isSelecting;
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: collection.filterChangeNotifier,
|
animation: collection.filterChangeNotifier,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final removableFilters = appMode != AppMode.pickInternal;
|
final removableFilters = appMode != AppMode.pickInternal;
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
leading: appMode.hasDrawer ? _buildAppBarLeading() : null,
|
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
|
||||||
title: _buildAppBarTitle(),
|
title: _buildAppBarTitle(isSelecting),
|
||||||
actions: _buildActions(),
|
actions: _buildActions(isSelecting),
|
||||||
bottom: hasFilters
|
bottom: hasFilters
|
||||||
? FilterBar(
|
? FilterBar(
|
||||||
filters: collection.filters,
|
filters: collection.filters,
|
||||||
|
@ -123,15 +126,15 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAppBarLeading() {
|
Widget _buildAppBarLeading(bool isSelecting) {
|
||||||
VoidCallback? onPressed;
|
VoidCallback? onPressed;
|
||||||
String? tooltip;
|
String? tooltip;
|
||||||
if (collection.isBrowsing) {
|
if (isSelecting) {
|
||||||
|
onPressed = () => context.read<Selection<AvesEntry>>().browse();
|
||||||
|
tooltip = MaterialLocalizations.of(context).backButtonTooltip;
|
||||||
|
} else {
|
||||||
onPressed = Scaffold.of(context).openDrawer;
|
onPressed = Scaffold.of(context).openDrawer;
|
||||||
tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip;
|
tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip;
|
||||||
} else if (collection.isSelecting) {
|
|
||||||
onPressed = collection.browse;
|
|
||||||
tooltip = MaterialLocalizations.of(context).backButtonTooltip;
|
|
||||||
}
|
}
|
||||||
return IconButton(
|
return IconButton(
|
||||||
key: const Key('appbar-leading-button'),
|
key: const Key('appbar-leading-button'),
|
||||||
|
@ -144,8 +147,13 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _buildAppBarTitle() {
|
Widget? _buildAppBarTitle(bool isSelecting) {
|
||||||
if (collection.isBrowsing) {
|
if (isSelecting) {
|
||||||
|
return Selector<Selection<AvesEntry>, int>(
|
||||||
|
selector: (context, selection) => selection.selection.length,
|
||||||
|
builder: (context, count, child) => Text(context.l10n.collectionSelectionPageTitle(count)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
Widget title = Text(appMode.isPicking ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle);
|
Widget title = Text(appMode.isPicking ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle);
|
||||||
if (appMode == AppMode.main) {
|
if (appMode == AppMode.main) {
|
||||||
|
@ -158,36 +166,25 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
onTap: appMode.canSearch ? _goToSearch : null,
|
onTap: appMode.canSearch ? _goToSearch : null,
|
||||||
child: title,
|
child: title,
|
||||||
);
|
);
|
||||||
} else if (collection.isSelecting) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: collection.selectionChangeNotifier,
|
|
||||||
builder: (context, child) {
|
|
||||||
final count = collection.selection.length;
|
|
||||||
return Text(context.l10n.collectionSelectionPageTitle(count));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildActions() {
|
List<Widget> _buildActions(bool isSelecting) {
|
||||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
return [
|
return [
|
||||||
if (collection.isBrowsing && appMode.canSearch)
|
if (!isSelecting && appMode.canSearch)
|
||||||
CollectionSearchButton(
|
CollectionSearchButton(
|
||||||
source: source,
|
source: source,
|
||||||
parentCollection: collection,
|
parentCollection: collection,
|
||||||
),
|
),
|
||||||
if (collection.isSelecting)
|
if (isSelecting)
|
||||||
...EntryActions.selection.map((action) => AnimatedBuilder(
|
...EntryActions.selection.map((action) => Selector<Selection<AvesEntry>, bool>(
|
||||||
animation: collection.selectionChangeNotifier,
|
selector: (context, selection) => selection.selection.isEmpty,
|
||||||
builder: (context, child) {
|
builder: (context, isEmpty, child) => IconButton(
|
||||||
return IconButton(
|
|
||||||
icon: Icon(action.getIcon()),
|
icon: Icon(action.getIcon()),
|
||||||
onPressed: collection.selection.isEmpty ? null : () => _actionDelegate.onEntryActionSelected(context, action),
|
onPressed: isEmpty ? null : () => _actionDelegate.onEntryActionSelected(context, action),
|
||||||
tooltip: action.getText(context),
|
tooltip: action.getText(context),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
FutureBuilder<bool>(
|
FutureBuilder<bool>(
|
||||||
future: _canAddShortcutsLoader,
|
future: _canAddShortcutsLoader,
|
||||||
|
@ -196,8 +193,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
return PopupMenuButton<CollectionAction>(
|
return PopupMenuButton<CollectionAction>(
|
||||||
key: const Key('appbar-menu-button'),
|
key: const Key('appbar-menu-button'),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
final isNotEmpty = !collection.isEmpty;
|
final isNotEmpty = !collection.isEmpty;
|
||||||
final hasSelection = collection.selection.isNotEmpty;
|
final hasSelection = selection.selection.isNotEmpty;
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
key: const Key('menu-sort'),
|
key: const Key('menu-sort'),
|
||||||
|
@ -210,7 +208,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
value: CollectionAction.group,
|
value: CollectionAction.group,
|
||||||
child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group),
|
child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group),
|
||||||
),
|
),
|
||||||
if (collection.isBrowsing && appMode == AppMode.main) ...[
|
if (!selection.isSelecting && appMode == AppMode.main) ...[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: CollectionAction.select,
|
value: CollectionAction.select,
|
||||||
enabled: isNotEmpty,
|
enabled: isNotEmpty,
|
||||||
|
@ -227,7 +225,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
child: MenuRow(text: context.l10n.collectionActionAddShortcut, icon: AIcons.addShortcut),
|
child: MenuRow(text: context.l10n.collectionActionAddShortcut, icon: AIcons.addShortcut),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (collection.isSelecting) ...[
|
if (selection.isSelecting) ...[
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: CollectionAction.copy,
|
value: CollectionAction.copy,
|
||||||
|
@ -247,7 +245,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: CollectionAction.selectAll,
|
value: CollectionAction.selectAll,
|
||||||
enabled: collection.selection.length < collection.entryCount,
|
enabled: selection.selection.length < collection.entryCount,
|
||||||
child: MenuRow(text: context.l10n.collectionActionSelectAll),
|
child: MenuRow(text: context.l10n.collectionActionSelectAll),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
|
@ -269,7 +267,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onActivityChange() {
|
void _onActivityChange() {
|
||||||
if (collection.isSelecting) {
|
if (context.read<Selection<AvesEntry>>().isSelecting) {
|
||||||
_browseToSelectAnimation.forward();
|
_browseToSelectAnimation.forward();
|
||||||
} else {
|
} else {
|
||||||
_browseToSelectAnimation.reverse();
|
_browseToSelectAnimation.reverse();
|
||||||
|
@ -289,13 +287,13 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
_actionDelegate.onCollectionActionSelected(context, action);
|
_actionDelegate.onCollectionActionSelected(context, action);
|
||||||
break;
|
break;
|
||||||
case CollectionAction.select:
|
case CollectionAction.select:
|
||||||
collection.select();
|
context.read<Selection<AvesEntry>>().select();
|
||||||
break;
|
break;
|
||||||
case CollectionAction.selectAll:
|
case CollectionAction.selectAll:
|
||||||
collection.addToSelection(collection.sortedEntries);
|
context.read<Selection<AvesEntry>>().addToSelection(collection.sortedEntries);
|
||||||
break;
|
break;
|
||||||
case CollectionAction.selectNone:
|
case CollectionAction.selectNone:
|
||||||
collection.clearSelection();
|
context.read<Selection<AvesEntry>>().clearSelection();
|
||||||
break;
|
break;
|
||||||
case CollectionAction.stats:
|
case CollectionAction.stats:
|
||||||
_goToStats();
|
_goToStats();
|
||||||
|
|
|
@ -12,7 +12,6 @@ import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/collection/app_bar.dart';
|
import 'package:aves/widgets/collection/app_bar.dart';
|
||||||
import 'package:aves/widgets/collection/draggable_thumb_label.dart';
|
import 'package:aves/widgets/collection/draggable_thumb_label.dart';
|
||||||
import 'package:aves/widgets/collection/grid/section_layout.dart';
|
import 'package:aves/widgets/collection/grid/section_layout.dart';
|
||||||
import 'package:aves/widgets/collection/grid/selector.dart';
|
|
||||||
import 'package:aves/widgets/collection/grid/thumbnail.dart';
|
import 'package:aves/widgets/collection/grid/thumbnail.dart';
|
||||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||||
import 'package:aves/widgets/collection/thumbnail/theme.dart';
|
import 'package:aves/widgets/collection/thumbnail/theme.dart';
|
||||||
|
@ -22,6 +21,7 @@ import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/selector.dart';
|
||||||
import 'package:aves/widgets/common/grid/sliver.dart';
|
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||||
import 'package:aves/widgets/common/identity/empty.dart';
|
import 'package:aves/widgets/common/identity/empty.dart';
|
||||||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||||
|
@ -173,7 +173,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
|
||||||
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
|
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
|
||||||
final selector = GridSelectionGestureDetector(
|
final selector = GridSelectionGestureDetector(
|
||||||
selectable: isMainMode,
|
selectable: isMainMode,
|
||||||
collection: collection,
|
entries: collection.sortedEntries,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
appBarHeightNotifier: appBarHeightNotifier,
|
appBarHeightNotifier: appBarHeightNotifier,
|
||||||
child: scaler,
|
child: scaler,
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||||
import 'package:aves/widgets/drawer/app_drawer.dart';
|
import 'package:aves/widgets/drawer/app_drawer.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -35,10 +38,13 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: WillPopScope(
|
body: SelectionProvider<AvesEntry>(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => WillPopScope(
|
||||||
onWillPop: () {
|
onWillPop: () {
|
||||||
if (collection.isSelecting) {
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
collection.browse();
|
if (selection.isSelecting) {
|
||||||
|
selection.browse();
|
||||||
return SynchronousFuture(false);
|
return SynchronousFuture(false);
|
||||||
}
|
}
|
||||||
return SynchronousFuture(true);
|
return SynchronousFuture(true);
|
||||||
|
@ -57,6 +63,8 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
),
|
),
|
||||||
|
|
|
@ -3,8 +3,10 @@ import 'dart:async';
|
||||||
import 'package:aves/model/actions/collection_actions.dart';
|
import 'package:aves/model/actions/collection_actions.dart';
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/image_op_events.dart';
|
import 'package:aves/services/image_op_events.dart';
|
||||||
|
@ -31,8 +33,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
_showDeleteDialog(context);
|
_showDeleteDialog(context);
|
||||||
break;
|
break;
|
||||||
case EntryAction.share:
|
case EntryAction.share:
|
||||||
final collection = context.read<CollectionLens>();
|
final selection = context.read<Selection<AvesEntry>>().selection;
|
||||||
AndroidAppService.shareEntries(collection.selection).then((success) {
|
AndroidAppService.shareEntries(selection).then((success) {
|
||||||
if (!success) showNoMatchingAppDialog(context);
|
if (!success) showNoMatchingAppDialog(context);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -59,16 +61,18 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
|
|
||||||
void _refreshMetadata(BuildContext context) {
|
void _refreshMetadata(BuildContext context) {
|
||||||
final collection = context.read<CollectionLens>();
|
final collection = context.read<CollectionLens>();
|
||||||
collection.source.refreshMetadata(collection.selection);
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
collection.browse();
|
collection.source.refreshMetadata(selection.selection);
|
||||||
|
selection.browse();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _moveSelection(BuildContext context, {required MoveType moveType}) async {
|
Future<void> _moveSelection(BuildContext context, {required MoveType moveType}) async {
|
||||||
final collection = context.read<CollectionLens>();
|
final collection = context.read<CollectionLens>();
|
||||||
final source = collection.source;
|
final source = collection.source;
|
||||||
final selection = collection.selection;
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
final selectedItems = selection.selection;
|
||||||
|
|
||||||
final selectionDirs = selection.map((e) => e.directory).whereNotNull().toSet();
|
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
|
||||||
if (moveType == MoveType.move) {
|
if (moveType == MoveType.move) {
|
||||||
// check whether moving is possible given OS restrictions,
|
// check whether moving is possible given OS restrictions,
|
||||||
// before asking to pick a destination album
|
// before asking to pick a destination album
|
||||||
|
@ -95,11 +99,11 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
|
|
||||||
if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs)) return;
|
if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs)) return;
|
||||||
|
|
||||||
if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, moveType)) return;
|
if (!await checkFreeSpaceForMove(context, selectedItems, destinationAlbum, moveType)) return;
|
||||||
|
|
||||||
// do not directly use selection when moving and post-processing items
|
// do not directly use selection when moving and post-processing items
|
||||||
// as source monitoring may remove obsolete items from the original selection
|
// as source monitoring may remove obsolete items from the original selection
|
||||||
final todoEntries = selection.toSet();
|
final todoEntries = selectedItems.toSet();
|
||||||
|
|
||||||
final copy = moveType == MoveType.copy;
|
final copy = moveType == MoveType.copy;
|
||||||
final todoCount = todoEntries.length;
|
final todoCount = todoEntries.length;
|
||||||
|
@ -118,7 +122,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
destinationAlbum: destinationAlbum,
|
destinationAlbum: destinationAlbum,
|
||||||
movedOps: movedOps,
|
movedOps: movedOps,
|
||||||
);
|
);
|
||||||
collection.browse();
|
selection.browse();
|
||||||
source.resumeMonitoring();
|
source.resumeMonitoring();
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
|
@ -177,9 +181,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
Future<void> _showDeleteDialog(BuildContext context) async {
|
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||||
final collection = context.read<CollectionLens>();
|
final collection = context.read<CollectionLens>();
|
||||||
final source = collection.source;
|
final source = collection.source;
|
||||||
final selection = collection.selection;
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
final selectionDirs = selection.map((e) => e.directory).whereNotNull().toSet();
|
final selectedItems = selection.selection;
|
||||||
final todoCount = selection.length;
|
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
|
||||||
|
final todoCount = selectedItems.length;
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -207,12 +212,12 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
source.pauseMonitoring();
|
source.pauseMonitoring();
|
||||||
showOpReport<ImageOpEvent>(
|
showOpReport<ImageOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
opStream: imageFileService.delete(selection),
|
opStream: imageFileService.delete(selectedItems),
|
||||||
itemCount: todoCount,
|
itemCount: todoCount,
|
||||||
onDone: (processed) async {
|
onDone: (processed) async {
|
||||||
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
|
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
|
||||||
await source.removeEntries(deletedUris);
|
await source.removeEntries(deletedUris);
|
||||||
collection.browse();
|
selection.browse();
|
||||||
source.resumeMonitoring();
|
source.resumeMonitoring();
|
||||||
|
|
||||||
final deletedCount = deletedUris.length;
|
final deletedCount = deletedUris.length;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/services/viewer_service.dart';
|
import 'package:aves/services/viewer_service.dart';
|
||||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||||
|
@ -31,10 +32,11 @@ class InteractiveThumbnail extends StatelessWidget {
|
||||||
final appMode = context.read<ValueNotifier<AppMode>>().value;
|
final appMode = context.read<ValueNotifier<AppMode>>().value;
|
||||||
switch (appMode) {
|
switch (appMode) {
|
||||||
case AppMode.main:
|
case AppMode.main:
|
||||||
if (collection.isBrowsing) {
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
if (selection.isSelecting) {
|
||||||
|
selection.toggleSelection(entry);
|
||||||
|
} else {
|
||||||
_goToViewer(context);
|
_goToViewer(context);
|
||||||
} else if (collection.isSelecting) {
|
|
||||||
collection.toggleSelection(entry);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AppMode.pickExternal:
|
case AppMode.pickExternal:
|
||||||
|
|
|
@ -2,8 +2,7 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/collection/thumbnail/theme.dart';
|
import 'package:aves/widgets/collection/thumbnail/theme.dart';
|
||||||
|
@ -59,19 +58,15 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final collection = context.watch<CollectionLens>();
|
final isSelecting = context.select<Selection<AvesEntry>, bool>((selection) => selection.isSelecting);
|
||||||
return ValueListenableBuilder<Activity>(
|
final child = isSelecting
|
||||||
valueListenable: collection.activityNotifier,
|
? Selector<Selection<AvesEntry>, bool>(
|
||||||
builder: (context, activity, child) {
|
selector: (context, selection) => selection.isSelected([entry]),
|
||||||
final child = collection.isSelecting
|
builder: (context, isSelected, child) {
|
||||||
? AnimatedBuilder(
|
var child = isSelecting
|
||||||
animation: collection.selectionChangeNotifier,
|
|
||||||
builder: (context, child) {
|
|
||||||
final selected = collection.isSelected([entry]);
|
|
||||||
var child = collection.isSelecting
|
|
||||||
? OverlayIcon(
|
? OverlayIcon(
|
||||||
key: ValueKey(selected),
|
key: ValueKey(isSelected),
|
||||||
icon: selected ? AIcons.selected : AIcons.unselected,
|
icon: isSelected ? AIcons.selected : AIcons.unselected,
|
||||||
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink();
|
: const SizedBox.shrink();
|
||||||
|
@ -88,7 +83,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
|
||||||
child = AnimatedContainer(
|
child = AnimatedContainer(
|
||||||
duration: duration,
|
duration: duration,
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
color: selected ? Colors.black54 : Colors.transparent,
|
color: isSelected ? Colors.black54 : Colors.transparent,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
return child;
|
return child;
|
||||||
|
@ -99,8 +94,6 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
|
||||||
duration: duration,
|
duration: duration,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
|
||||||
import 'package:aves/model/source/section_keys.dart';
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
@ -79,11 +80,12 @@ class SectionHeader extends StatelessWidget {
|
||||||
void _toggleSectionSelection(BuildContext context) {
|
void _toggleSectionSelection(BuildContext context) {
|
||||||
final collection = context.read<CollectionLens>();
|
final collection = context.read<CollectionLens>();
|
||||||
final sectionEntries = collection.sections[sectionKey]!;
|
final sectionEntries = collection.sections[sectionKey]!;
|
||||||
final selected = collection.isSelected(sectionEntries);
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
if (selected) {
|
final isSelected = selection.isSelected(sectionEntries);
|
||||||
collection.removeFromSelection(sectionEntries);
|
if (isSelected) {
|
||||||
|
selection.removeFromSelection(sectionEntries);
|
||||||
} else {
|
} else {
|
||||||
collection.addToSelection(sectionEntries);
|
selection.addToSelection(sectionEntries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,47 +144,14 @@ class _SectionSelectableLeading extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!selectable) return _buildBrowsing(context);
|
if (!selectable) return _buildBrowsing(context);
|
||||||
|
|
||||||
final collection = context.watch<CollectionLens>();
|
final isSelecting = context.select<Selection<AvesEntry>, bool>((selection) => selection.isSelecting);
|
||||||
return ValueListenableBuilder<Activity>(
|
final Widget child = isSelecting
|
||||||
valueListenable: collection.activityNotifier,
|
? _SectionSelectingLeading(
|
||||||
builder: (context, activity, child) {
|
sectionKey: sectionKey,
|
||||||
final child = collection.isSelecting
|
|
||||||
? AnimatedBuilder(
|
|
||||||
animation: collection.selectionChangeNotifier,
|
|
||||||
builder: (context, child) {
|
|
||||||
final sectionEntries = collection.sections[sectionKey]!;
|
|
||||||
final selected = collection.isSelected(sectionEntries);
|
|
||||||
final child = TooltipTheme(
|
|
||||||
key: ValueKey(selected),
|
|
||||||
data: TooltipTheme.of(context).copyWith(
|
|
||||||
preferBelow: false,
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
iconSize: 26,
|
|
||||||
padding: const EdgeInsets.only(top: 1),
|
|
||||||
alignment: AlignmentDirectional.topStart,
|
|
||||||
icon: Icon(selected ? AIcons.selected : AIcons.unselected),
|
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
tooltip: selected ? context.l10n.collectionDeselectSectionTooltip : context.l10n.collectionSelectSectionTooltip,
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minHeight: leadingDimension,
|
|
||||||
minWidth: leadingDimension,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: Durations.sectionHeaderAnimation,
|
|
||||||
switchInCurve: Curves.easeOutBack,
|
|
||||||
switchOutCurve: Curves.easeOutBack,
|
|
||||||
transitionBuilder: (child, animation) => ScaleTransition(
|
|
||||||
scale: animation,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
: _buildBrowsing(context);
|
: _buildBrowsing(context);
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return AnimatedSwitcher(
|
||||||
duration: Durations.sectionHeaderAnimation,
|
duration: Durations.sectionHeaderAnimation,
|
||||||
switchInCurve: Curves.easeInOut,
|
switchInCurve: Curves.easeInOut,
|
||||||
|
@ -205,9 +174,52 @@ class _SectionSelectableLeading extends StatelessWidget {
|
||||||
},
|
},
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? const SizedBox(height: leadingDimension);
|
Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? const SizedBox(height: leadingDimension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SectionSelectingLeading extends StatelessWidget {
|
||||||
|
final SectionKey sectionKey;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
const _SectionSelectingLeading({
|
||||||
|
Key? key,
|
||||||
|
required this.sectionKey,
|
||||||
|
required this.onPressed,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final sectionEntries = context.watch<CollectionLens>().sections[sectionKey]!;
|
||||||
|
final selection = context.watch<Selection<AvesEntry>>();
|
||||||
|
final isSelected = selection.isSelected(sectionEntries);
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: Durations.sectionHeaderAnimation,
|
||||||
|
switchInCurve: Curves.easeOutBack,
|
||||||
|
switchOutCurve: Curves.easeOutBack,
|
||||||
|
transitionBuilder: (child, animation) => ScaleTransition(
|
||||||
|
scale: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: TooltipTheme(
|
||||||
|
key: ValueKey(isSelected),
|
||||||
|
data: TooltipTheme.of(context).copyWith(
|
||||||
|
preferBelow: false,
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
iconSize: 26,
|
||||||
|
padding: const EdgeInsets.only(top: 1),
|
||||||
|
alignment: AlignmentDirectional.topStart,
|
||||||
|
icon: Icon(isSelected ? AIcons.selected : AIcons.unselected),
|
||||||
|
onPressed: onPressed,
|
||||||
|
tooltip: isSelected ? context.l10n.collectionDeselectSectionTooltip : context.l10n.collectionSelectSectionTooltip,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minHeight: SectionHeader.leadingDimension,
|
||||||
|
minWidth: SectionHeader.leadingDimension,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||||
|
@ -10,9 +9,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class GridSelectionGestureDetector extends StatefulWidget {
|
class GridSelectionGestureDetector<T> extends StatefulWidget {
|
||||||
final bool selectable;
|
final bool selectable;
|
||||||
final CollectionLens collection;
|
final List<T> entries;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
final ValueNotifier<double> appBarHeightNotifier;
|
final ValueNotifier<double> appBarHeightNotifier;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -20,17 +19,17 @@ class GridSelectionGestureDetector extends StatefulWidget {
|
||||||
const GridSelectionGestureDetector({
|
const GridSelectionGestureDetector({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.selectable = true,
|
this.selectable = true,
|
||||||
required this.collection,
|
required this.entries,
|
||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
required this.appBarHeightNotifier,
|
required this.appBarHeightNotifier,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_GridSelectionGestureDetectorState createState() => _GridSelectionGestureDetectorState();
|
_GridSelectionGestureDetectorState createState() => _GridSelectionGestureDetectorState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GridSelectionGestureDetectorState extends State<GridSelectionGestureDetector> {
|
class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDetector<T>> {
|
||||||
bool _pressing = false, _selecting = false;
|
bool _pressing = false, _selecting = false;
|
||||||
late int _fromIndex, _lastToIndex;
|
late int _fromIndex, _lastToIndex;
|
||||||
late Offset _localPosition;
|
late Offset _localPosition;
|
||||||
|
@ -38,9 +37,7 @@ class _GridSelectionGestureDetectorState extends State<GridSelectionGestureDetec
|
||||||
late double _scrollSpeedFactor;
|
late double _scrollSpeedFactor;
|
||||||
Timer? _updateTimer;
|
Timer? _updateTimer;
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
List<T> get entries => widget.entries;
|
||||||
|
|
||||||
List<AvesEntry> get entries => collection.sortedEntries;
|
|
||||||
|
|
||||||
ScrollController get scrollController => widget.scrollController;
|
ScrollController get scrollController => widget.scrollController;
|
||||||
|
|
||||||
|
@ -58,8 +55,9 @@ class _GridSelectionGestureDetectorState extends State<GridSelectionGestureDetec
|
||||||
final fromEntry = _getEntryAt(details.localPosition);
|
final fromEntry = _getEntryAt(details.localPosition);
|
||||||
if (fromEntry == null) return;
|
if (fromEntry == null) return;
|
||||||
|
|
||||||
collection.toggleSelection(fromEntry);
|
final selection = context.read<Selection<T>>();
|
||||||
_selecting = collection.isSelected([fromEntry]);
|
selection.toggleSelection(fromEntry);
|
||||||
|
_selecting = selection.isSelected([fromEntry]);
|
||||||
_fromIndex = entries.indexOf(fromEntry);
|
_fromIndex = entries.indexOf(fromEntry);
|
||||||
_lastToIndex = _fromIndex;
|
_lastToIndex = _fromIndex;
|
||||||
_scrollableInsets = EdgeInsets.only(
|
_scrollableInsets = EdgeInsets.only(
|
||||||
|
@ -134,41 +132,42 @@ class _GridSelectionGestureDetectorState extends State<GridSelectionGestureDetec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AvesEntry? _getEntryAt(Offset localPosition) {
|
T? _getEntryAt(Offset localPosition) {
|
||||||
// as of Flutter v1.22.5, `hitTest` on the `ScrollView` render object works fine when it is static,
|
// as of Flutter v1.22.5, `hitTest` on the `ScrollView` render object works fine when it is static,
|
||||||
// but when it is scrolling (through controller animation), result is incomplete and children are missing,
|
// but when it is scrolling (through controller animation), result is incomplete and children are missing,
|
||||||
// so we use custom layout computation instead to find the entry.
|
// so we use custom layout computation instead to find the entry.
|
||||||
final offset = Offset(0, scrollController.offset - appBarHeight) + localPosition;
|
final offset = Offset(0, scrollController.offset - appBarHeight) + localPosition;
|
||||||
final sectionedListLayout = context.read<SectionedListLayout<AvesEntry>>();
|
final sectionedListLayout = context.read<SectionedListLayout<T>>();
|
||||||
return sectionedListLayout.getItemAt(offset);
|
return sectionedListLayout.getItemAt(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleSelectionToIndex(int toIndex) {
|
void _toggleSelectionToIndex(int toIndex) {
|
||||||
if (toIndex == -1) return;
|
if (toIndex == -1) return;
|
||||||
|
|
||||||
|
final selection = context.read<Selection<T>>();
|
||||||
if (_selecting) {
|
if (_selecting) {
|
||||||
if (toIndex <= _fromIndex) {
|
if (toIndex <= _fromIndex) {
|
||||||
if (toIndex < _lastToIndex) {
|
if (toIndex < _lastToIndex) {
|
||||||
collection.addToSelection(entries.getRange(toIndex, min(_fromIndex, _lastToIndex)));
|
selection.addToSelection(entries.getRange(toIndex, min(_fromIndex, _lastToIndex)));
|
||||||
if (_fromIndex < _lastToIndex) {
|
if (_fromIndex < _lastToIndex) {
|
||||||
collection.removeFromSelection(entries.getRange(_fromIndex + 1, _lastToIndex + 1));
|
selection.removeFromSelection(entries.getRange(_fromIndex + 1, _lastToIndex + 1));
|
||||||
}
|
}
|
||||||
} else if (_lastToIndex < toIndex) {
|
} else if (_lastToIndex < toIndex) {
|
||||||
collection.removeFromSelection(entries.getRange(_lastToIndex, toIndex));
|
selection.removeFromSelection(entries.getRange(_lastToIndex, toIndex));
|
||||||
}
|
}
|
||||||
} else if (_fromIndex < toIndex) {
|
} else if (_fromIndex < toIndex) {
|
||||||
if (_lastToIndex < toIndex) {
|
if (_lastToIndex < toIndex) {
|
||||||
collection.addToSelection(entries.getRange(max(_fromIndex, _lastToIndex), toIndex + 1));
|
selection.addToSelection(entries.getRange(max(_fromIndex, _lastToIndex), toIndex + 1));
|
||||||
if (_lastToIndex < _fromIndex) {
|
if (_lastToIndex < _fromIndex) {
|
||||||
collection.removeFromSelection(entries.getRange(_lastToIndex, _fromIndex));
|
selection.removeFromSelection(entries.getRange(_lastToIndex, _fromIndex));
|
||||||
}
|
}
|
||||||
} else if (toIndex < _lastToIndex) {
|
} else if (toIndex < _lastToIndex) {
|
||||||
collection.removeFromSelection(entries.getRange(toIndex + 1, _lastToIndex + 1));
|
selection.removeFromSelection(entries.getRange(toIndex + 1, _lastToIndex + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_lastToIndex = toIndex;
|
_lastToIndex = toIndex;
|
||||||
} else {
|
} else {
|
||||||
collection.removeFromSelection(entries.getRange(min(_fromIndex, toIndex), max(_fromIndex, toIndex) + 1));
|
selection.removeFromSelection(entries.getRange(min(_fromIndex, toIndex), max(_fromIndex, toIndex) + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
20
lib/widgets/common/providers/selection_provider.dart
Normal file
20
lib/widgets/common/providers/selection_provider.dart
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class SelectionProvider<T> extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const SelectionProvider({
|
||||||
|
Key? key,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProvider<Selection<T>>(
|
||||||
|
create: (context) => Selection<T>(),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -36,7 +38,8 @@ class _ItemPickDialogState extends State<ItemPickDialog> {
|
||||||
value: ValueNotifier(AppMode.pickInternal),
|
value: ValueNotifier(AppMode.pickInternal),
|
||||||
child: MediaQueryDataProvider(
|
child: MediaQueryDataProvider(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: GestureAreaProtectorStack(
|
body: SelectionProvider<AvesEntry>(
|
||||||
|
child: GestureAreaProtectorStack(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: ChangeNotifierProvider<CollectionLens>.value(
|
child: ChangeNotifierProvider<CollectionLens>.value(
|
||||||
|
@ -49,6 +52,7 @@ class _ItemPickDialogState extends State<ItemPickDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue