diff --git a/CHANGELOG.md b/CHANGELOG.md index e409b9f58..0ecb54172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- Map & Stats from selection ## [v1.4.8] - 2021-08-08 ### Added diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9c666f5a5..7ea56840c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -321,6 +321,12 @@ "@menuActionSort": {}, "menuActionGroup": "Group", "@menuActionGroup": {}, + "menuActionSelect": "Select", + "@menuActionSelect": {}, + "menuActionSelectAll": "Select all", + "@menuActionSelectAll": {}, + "menuActionSelectNone": "Select none", + "@menuActionSelectNone": {}, "menuActionMap": "Map", "@menuActionMap": {}, "menuActionStats": "Stats", @@ -376,12 +382,6 @@ "collectionActionAddShortcut": "Add shortcut", "@collectionActionAddShortcut": {}, - "collectionActionSelect": "Select", - "@collectionActionSelect": {}, - "collectionActionSelectAll": "Select all", - "@collectionActionSelectAll": {}, - "collectionActionSelectNone": "Select none", - "@collectionActionSelectNone": {}, "collectionActionCopy": "Copy to album", "@collectionActionCopy": {}, "collectionActionMove": "Move to album", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 3622d2ec7..402f6a550 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -147,6 +147,9 @@ "menuActionSort": "정렬", "menuActionGroup": "묶음", + "menuActionSelect": "선택", + "menuActionSelectAll": "모두 선택", + "menuActionSelectNone": "모두 해제", "menuActionMap": "지도", "menuActionStats": "통계", @@ -174,9 +177,6 @@ "collectionSelectionPageTitle": "{count, plural, =0{항목 선택} other{{count}개}}", "collectionActionAddShortcut": "홈 화면에 추가", - "collectionActionSelect": "선택", - "collectionActionSelectAll": "모두 선택", - "collectionActionSelectNone": "모두 해제", "collectionActionCopy": "앨범으로 복사", "collectionActionMove": "앨범으로 이동", "collectionActionRefreshMetadata": "새로 분석", diff --git a/lib/model/actions/chip_set_actions.dart b/lib/model/actions/chip_set_actions.dart index 5fdbc2e3d..8ce4fe066 100644 --- a/lib/model/actions/chip_set_actions.dart +++ b/lib/model/actions/chip_set_actions.dart @@ -6,18 +6,19 @@ enum ChipSetAction { // general sort, group, - map, select, selectAll, selectNone, - stats, createAlbum, - // single/multiple filters + // all or filter selection + map, + stats, + // single/multiple filter selection delete, hide, pin, unpin, - // single filter + // single filter selection rename, setCover, } @@ -31,11 +32,11 @@ extension ExtraChipSetAction on ChipSetAction { case ChipSetAction.group: return context.l10n.menuActionGroup; case ChipSetAction.select: - return context.l10n.collectionActionSelect; + return context.l10n.menuActionSelect; case ChipSetAction.selectAll: - return context.l10n.collectionActionSelectAll; + return context.l10n.menuActionSelectAll; case ChipSetAction.selectNone: - return context.l10n.collectionActionSelectNone; + return context.l10n.menuActionSelectNone; case ChipSetAction.map: return context.l10n.menuActionMap; case ChipSetAction.stats: @@ -69,8 +70,9 @@ extension ExtraChipSetAction on ChipSetAction { case ChipSetAction.select: return AIcons.select; case ChipSetAction.selectAll: + return AIcons.selected; case ChipSetAction.selectNone: - return null; + return AIcons.unselected; case ChipSetAction.map: return AIcons.map; case ChipSetAction.stats: diff --git a/lib/model/actions/collection_actions.dart b/lib/model/actions/collection_actions.dart index 45aa9b110..27303390e 100644 --- a/lib/model/actions/collection_actions.dart +++ b/lib/model/actions/collection_actions.dart @@ -1,14 +1,85 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + enum CollectionAction { - addShortcut, + // general sort, group, select, selectAll, selectNone, + // all + addShortcut, + // all or entry selection map, stats, - // apply to entry set + // entry selection copy, move, refreshMetadata, } + +extension ExtraCollectionAction on CollectionAction { + String getText(BuildContext context) { + switch (this) { + // general + case CollectionAction.sort: + return context.l10n.menuActionSort; + case CollectionAction.group: + return context.l10n.menuActionGroup; + case CollectionAction.select: + return context.l10n.menuActionSelect; + case CollectionAction.selectAll: + return context.l10n.menuActionSelectAll; + case CollectionAction.selectNone: + return context.l10n.menuActionSelectNone; + // all + case CollectionAction.addShortcut: + return context.l10n.collectionActionAddShortcut; + // all or entry selection + case CollectionAction.map: + return context.l10n.menuActionMap; + case CollectionAction.stats: + return context.l10n.menuActionStats; + // entry selection + case CollectionAction.copy: + return context.l10n.collectionActionCopy; + case CollectionAction.move: + return context.l10n.collectionActionMove; + case CollectionAction.refreshMetadata: + return context.l10n.collectionActionRefreshMetadata; + } + } + + IconData? getIcon() { + switch (this) { + // general + case CollectionAction.sort: + return AIcons.sort; + case CollectionAction.group: + return AIcons.group; + case CollectionAction.select: + return AIcons.select; + case CollectionAction.selectAll: + return AIcons.selected; + case CollectionAction.selectNone: + return AIcons.unselected; + // all + case CollectionAction.addShortcut: + return AIcons.addShortcut; + // all or entry selection + case CollectionAction.map: + return AIcons.map; + case CollectionAction.stats: + return AIcons.stats; + // entry selection + case CollectionAction.copy: + return AIcons.copy; + case CollectionAction.move: + return AIcons.move; + case CollectionAction.refreshMetadata: + return AIcons.refresh; + } + } +} diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 656334fe7..3075531d2 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -37,6 +37,7 @@ class AIcons { static const IconData captureFrame = Icons.screenshot_outlined; static const IconData clear = Icons.clear_outlined; static const IconData clipboard = Icons.content_copy_outlined; + static const IconData copy = Icons.file_copy_outlined; static const IconData createAlbum = Icons.add_circle_outline; static const IconData debug = Icons.whatshot_outlined; static const IconData delete = Icons.delete_outlined; @@ -51,6 +52,7 @@ class AIcons { static const IconData info = Icons.info_outlined; static const IconData layers = Icons.layers_outlined; static const IconData map = Icons.map_outlined; + static const IconData move = MdiIcons.fileMoveOutline; static const IconData newTier = Icons.fiber_new_outlined; static const IconData openOutside = Icons.open_in_new_outlined; static const IconData pin = Icons.push_pin_outlined; @@ -58,6 +60,7 @@ class AIcons { static const IconData play = Icons.play_arrow; static const IconData pause = Icons.pause; static const IconData print = Icons.print_outlined; + static const IconData refresh = Icons.refresh_outlined; static const IconData rename = Icons.title_outlined; static const IconData rotateLeft = Icons.rotate_left_outlined; static const IconData rotateRight = Icons.rotate_right_outlined; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 22d2f2f00..29fc29aa0 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -12,7 +12,6 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/services/app_shortcut_service.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/utils/pedantic.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; @@ -22,10 +21,8 @@ import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/search/search_button.dart'; import 'package:aves/widgets/search/search_delegate.dart'; -import 'package:aves/widgets/stats/stats_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -192,70 +189,55 @@ class _CollectionAppBarState extends State with SingleTickerPr return PopupMenuButton( key: const Key('appbar-menu-button'), itemBuilder: (context) { + final groupable = collection.sortFactor == EntrySortFactor.date; final selection = context.read>(); - final isNotEmpty = !collection.isEmpty; - final hasSelection = selection.selection.isNotEmpty; + final isSelecting = selection.isSelecting; + final selectedItems = selection.selection; + final hasSelection = selectedItems.isNotEmpty; + final hasItems = !collection.isEmpty; + final otherViewEnabled = (!isSelecting && hasItems) || (isSelecting && hasSelection); + return [ - PopupMenuItem( + _toMenuItem( + CollectionAction.sort, key: const Key('menu-sort'), - value: CollectionAction.sort, - child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort), ), - if (collection.sortFactor == EntrySortFactor.date) - PopupMenuItem( + if (groupable) + _toMenuItem( + CollectionAction.group, key: const Key('menu-group'), - value: CollectionAction.group, - child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group), ), - if (!selection.isSelecting && appMode == AppMode.main) ...[ - PopupMenuItem( - value: CollectionAction.select, - enabled: isNotEmpty, - child: MenuRow(text: context.l10n.collectionActionSelect, icon: AIcons.select), - ), - PopupMenuItem( - value: CollectionAction.map, - enabled: isNotEmpty, - child: MenuRow(text: context.l10n.menuActionMap, icon: AIcons.map), - ), - PopupMenuItem( - value: CollectionAction.stats, - enabled: isNotEmpty, - child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats), - ), - if (canAddShortcuts) - PopupMenuItem( - value: CollectionAction.addShortcut, - child: MenuRow(text: context.l10n.collectionActionAddShortcut, icon: AIcons.addShortcut), + if (appMode == AppMode.main) ...[ + if (!isSelecting) + _toMenuItem( + CollectionAction.select, + enabled: hasItems, ), + const PopupMenuDivider(), + if (isSelecting) + ...[ + CollectionAction.copy, + CollectionAction.move, + CollectionAction.refreshMetadata, + ].map((v) => _toMenuItem(v, enabled: hasSelection)), + ...[ + CollectionAction.map, + CollectionAction.stats, + ].map((v) => _toMenuItem(v, enabled: otherViewEnabled)), + if (!isSelecting && canAddShortcuts) ...[ + const PopupMenuDivider(), + _toMenuItem(CollectionAction.addShortcut), + ], ], - if (selection.isSelecting) ...[ + if (isSelecting) ...[ const PopupMenuDivider(), - PopupMenuItem( - value: CollectionAction.copy, - enabled: hasSelection, - child: MenuRow(text: context.l10n.collectionActionCopy), + _toMenuItem( + CollectionAction.selectAll, + enabled: selectedItems.length < collection.entryCount, ), - PopupMenuItem( - value: CollectionAction.move, + _toMenuItem( + CollectionAction.selectNone, enabled: hasSelection, - child: MenuRow(text: context.l10n.collectionActionMove), - ), - PopupMenuItem( - value: CollectionAction.refreshMetadata, - enabled: hasSelection, - child: MenuRow(text: context.l10n.collectionActionRefreshMetadata), - ), - const PopupMenuDivider(), - PopupMenuItem( - value: CollectionAction.selectAll, - enabled: selection.selection.length < collection.entryCount, - child: MenuRow(text: context.l10n.collectionActionSelectAll), - ), - PopupMenuItem( - value: CollectionAction.selectNone, - enabled: hasSelection, - child: MenuRow(text: context.l10n.collectionActionSelectNone), ), ] ]; @@ -270,6 +252,18 @@ class _CollectionAppBarState extends State with SingleTickerPr ]; } + PopupMenuItem _toMenuItem(CollectionAction action, {Key? key, bool enabled = true}) { + return PopupMenuItem( + key: key, + value: action, + enabled: enabled, + child: MenuRow( + text: action.getText(context), + icon: action.getIcon(), + ), + ); + } + void _onActivityChange() { if (context.read>().isSelecting) { _browseToSelectAnimation.forward(); @@ -287,6 +281,8 @@ class _CollectionAppBarState extends State with SingleTickerPr case CollectionAction.copy: case CollectionAction.move: case CollectionAction.refreshMetadata: + case CollectionAction.map: + case CollectionAction.stats: _actionDelegate.onCollectionActionSelected(context, action); break; case CollectionAction.select: @@ -298,12 +294,6 @@ class _CollectionAppBarState extends State with SingleTickerPr case CollectionAction.selectNone: context.read>().clearSelection(); break; - case CollectionAction.map: - _goToMap(); - break; - case CollectionAction.stats: - _goToStats(); - break; case CollectionAction.addShortcut: unawaited(_showShortcutDialog(context)); break; @@ -385,30 +375,4 @@ class _CollectionAppBarState extends State with SingleTickerPr ), ); } - - void _goToMap() { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: MapPage.routeName), - builder: (context) => MapPage( - source: source, - parentCollection: collection, - ), - ), - ); - } - - void _goToStats() { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: StatsPage.routeName), - builder: (context) => StatsPage( - source: source, - parentCollection: collection, - ), - ), - ); - } } diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index de3a99bb1..b3818b5d7 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -22,6 +22,8 @@ import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/map/map_page.dart'; +import 'package:aves/widgets/stats/stats_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -52,6 +54,12 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case CollectionAction.refreshMetadata: _refreshMetadata(context); break; + case CollectionAction.map: + _goToMap(context); + break; + case CollectionAction.stats: + _goToStats(context); + break; default: break; } @@ -242,4 +250,37 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware }, ); } + + void _goToMap(BuildContext context) { + final selection = context.read>(); + final entries = selection.isSelecting ? _getExpandedSelectedItems(selection) : context.read().sortedEntries; + + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: MapPage.routeName), + builder: (context) => MapPage( + entries: entries.where((entry) => entry.hasGps).toList(), + ), + ), + ); + } + + void _goToStats(BuildContext context) { + final selection = context.read>(); + final collection = context.read(); + final entries = selection.isSelecting ? _getExpandedSelectedItems(selection) : collection.sortedEntries.toSet(); + + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: StatsPage.routeName), + builder: (context) => StatsPage( + entries: entries, + source: collection.source, + parentCollection: collection, + ), + ), + ); + } } diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index 1ff248eb8..97ed5389b 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -76,10 +76,10 @@ abstract class ChipSetActionDelegate with FeedbackMi _showSortDialog(context); break; case ChipSetAction.map: - _goToMap(context); + _goToMap(context, filters); break; case ChipSetAction.stats: - _goToStats(context); + _goToStats(context, filters); break; case ChipSetAction.select: context.read>>().select(); @@ -129,28 +129,33 @@ abstract class ChipSetActionDelegate with FeedbackMi } } - void _goToMap(BuildContext context) { + void _goToMap(BuildContext context, Set filters) { final source = context.read(); + final entries = filters.isEmpty ? source.visibleEntries : source.visibleEntries.where((entry) => filters.any((f) => f.test(entry))); Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: MapPage.routeName), builder: (context) => MapPage( - source: source, + entries: entries.where((entry) => entry.hasGps).toList(), ), ), ); } - void _goToStats(BuildContext context) { + void _goToStats(BuildContext context, Set filters) { final source = context.read(); + final entries = filters.isEmpty ? source.visibleEntries : source.visibleEntries.where((entry) => filters.any((f) => f.test(entry))); Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: StatsPage.routeName), - builder: (context) => StatsPage( - source: source, - ), + builder: (context) { + return StatsPage( + entries: entries.toSet(), + source: source, + ); + }, ), ); } diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index 53d188ebd..f3105afde 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -174,19 +174,44 @@ class _FilterGridAppBarState extends State( key: const Key('appbar-menu-button'), itemBuilder: (context) { + final selectedItems = selection.selection; + final hasSelection = selectedItems.isNotEmpty; + final hasItems = !widget.isEmpty; + final otherViewEnabled = (!isSelecting && hasItems) || (isSelecting && hasSelection); + final menuItems = >[ toMenuItem(ChipSetAction.sort), if (widget.groupable) toMenuItem(ChipSetAction.group), + if (appMode == AppMode.main && !isSelecting) + toMenuItem( + ChipSetAction.select, + enabled: hasItems, + ), ]; - if (isSelecting) { - final selectedItems = selection.selection; - - if (selectionRowActions.isNotEmpty) { - menuItems.add(const PopupMenuDivider()); + if (appMode == AppMode.main) { + menuItems.add(const PopupMenuDivider()); + if (isSelecting) { menuItems.addAll(selectionRowActions.map(toMenuItem)); } - + menuItems.addAll([ + toMenuItem( + ChipSetAction.map, + enabled: otherViewEnabled, + ), + toMenuItem( + ChipSetAction.stats, + enabled: otherViewEnabled, + ), + ]); + if (!isSelecting) { + menuItems.addAll([ + const PopupMenuDivider(), + toMenuItem(ChipSetAction.createAlbum), + ]); + } + } + if (isSelecting) { menuItems.addAll([ const PopupMenuDivider(), toMenuItem( @@ -195,19 +220,9 @@ class _FilterGridAppBarState extends State entries; + final List entries; - MapPage({ + const MapPage({ Key? key, - required this.source, - this.parentCollection, - }) : super(key: key) { - entries = (parentCollection?.sortedEntries.expand((entry) => entry.burstEntries ?? {entry}).toSet() ?? source.visibleEntries).where((entry) => entry.hasGps).toList(); - } + required this.entries, + }) : super(key: key); @override _MapPageState createState() => _MapPageState(); diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index b48570386..1e2aa466f 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -28,17 +28,17 @@ class StatsPage extends StatelessWidget { final CollectionSource source; final CollectionLens? parentCollection; - late final Set entries; + final Set entries; final Map entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {}; static const mimeDonutMinWidth = 124.0; StatsPage({ Key? key, + required this.entries, required this.source, this.parentCollection, }) : super(key: key) { - entries = parentCollection?.sortedEntries.expand((entry) => entry.burstEntries ?? {entry}).toSet() ?? source.visibleEntries; entries.forEach((entry) { if (entry.hasAddress) { final address = entry.addressDetails!;