diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c1717c39a..7016ee892 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -46,6 +46,7 @@ filters = call.argument("filters"); + pin(label, filters); + result.success(null); + break; + } + default: + result.notImplemented(); + break; + } + } + + private void pin(String label, @Nullable List filters) { + if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context) || filters == null) { + return; + } + + ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, "collection-" + TextUtils.join("-", filters)) + .setShortLabel(label) + .setIcon(IconCompat.createWithResource(context, R.mipmap.ic_shortcut_collection)) + .setIntent(new Intent(Intent.ACTION_MAIN, null, context, MainActivity.class) + .putExtra("page", "/collection") + .putExtra("filters", filters.toArray(new String[0]))) + .build(); + + ShortcutManagerCompat.requestPinShortcut(context, shortcut, null); + } +} diff --git a/android/app/src/main/res/drawable/ic_shortcut_collection_foreground.xml b/android/app/src/main/res/drawable/ic_shortcut_collection_foreground.xml new file mode 100644 index 000000000..45b1c8cb2 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_shortcut_collection_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_collection.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_collection.xml new file mode 100644 index 000000000..e069568b3 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_collection.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 7e7583bff..f729f2329 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -24,6 +24,7 @@ class AlbumFilter extends CollectionFilter { json['uniqueName'], ); + @override Map toJson() => { 'type': type, 'album': album, diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index 39d88b8ae..fbf1194de 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; class FavouriteFilter extends CollectionFilter { static const type = 'favourite'; + @override Map toJson() => { 'type': type, }; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index 4b198f094..ab519743c 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -45,6 +45,8 @@ abstract class CollectionFilter implements Comparable { const CollectionFilter(); + Map toJson(); + bool filter(ImageEntry entry); bool get isUnique => true; diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index e348d6459..1042430d2 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -23,6 +23,7 @@ class LocationFilter extends CollectionFilter { json['location'], ); + @override Map toJson() => { 'type': type, 'level': level.toString(), diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 6ae0ff1f9..54b6beb0a 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -43,6 +43,7 @@ class MimeFilter extends CollectionFilter { json['mime'], ); + @override Map toJson() => { 'type': type, 'mime': mime, diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index b9a9f915d..9714adc2b 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -37,6 +37,7 @@ class QueryFilter extends CollectionFilter { json['query'], ); + @override Map toJson() => { 'type': type, 'query': query, diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index a25b5d630..83c0d5ef1 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -15,6 +15,7 @@ class TagFilter extends CollectionFilter { json['tag'], ); + @override Map toJson() => { 'type': type, 'tag': tag, diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart new file mode 100644 index 000000000..9008776f3 --- /dev/null +++ b/lib/services/app_shortcut_service.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; + +import 'package:aves/model/filters/filters.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +class AppShortcutService { + static const platform = MethodChannel('deckers.thibault/aves/shortcut'); + + // this ability will not change over the lifetime of the app + static bool _canPin; + + static Future canPin() async { + if (_canPin != null) { + return SynchronousFuture(_canPin); + } + + try { + _canPin = await platform.invokeMethod('canPin'); + return _canPin; + } on PlatformException catch (e) { + debugPrint('canPin failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); + } + return false; + } + + static Future pin(String label, Set filters) async { + try { + await platform.invokeMethod('pin', { + 'label': label, + 'filters': filters.map((filter) => jsonEncode(filter.toJson())).toList(), + }); + } on PlatformException catch (e) { + debugPrint('pin failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + } +} diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index a89e81e47..97cf2583f 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -4,7 +4,9 @@ import 'package:aves/main.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/services/app_shortcut_service.dart'; import 'package:aves/utils/durations.dart'; +import 'package:aves/widgets/collection/collection_actions.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/collection/search/search_delegate.dart'; import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart'; @@ -39,6 +41,7 @@ class _CollectionAppBarState extends State with SingleTickerPr final TextEditingController _searchFieldController = TextEditingController(); SelectionActionDelegate _actionDelegate; AnimationController _browseToSelectAnimation; + Future _canAddShortcutsLoader; CollectionLens get collection => widget.collection; @@ -54,6 +57,7 @@ class _CollectionAppBarState extends State with SingleTickerPr duration: Durations.iconAnimation, vsync: this, ); + _canAddShortcutsLoader = AppShortcutService.canPin(); _registerWidget(widget); WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight()); } @@ -187,71 +191,80 @@ class _CollectionAppBarState extends State with SingleTickerPr ); }, )), - Builder( - builder: (context) => PopupMenuButton( - key: Key('appbar-menu-button'), - itemBuilder: (context) { - final hasSelection = collection.selection.isNotEmpty; - return [ - PopupMenuItem( - key: Key('menu-sort'), - value: CollectionAction.sort, - child: MenuRow(text: 'Sort...', icon: AIcons.sort), - ), - if (collection.sortFactor == EntrySortFactor.date) + FutureBuilder( + future: _canAddShortcutsLoader, + builder: (context, snapshot) { + final canAddShortcuts = snapshot.data ?? false; + return PopupMenuButton( + key: Key('appbar-menu-button'), + itemBuilder: (context) { + final hasSelection = collection.selection.isNotEmpty; + return [ PopupMenuItem( - key: Key('menu-group'), - value: CollectionAction.group, - child: MenuRow(text: 'Group...', icon: AIcons.group), + key: Key('menu-sort'), + value: CollectionAction.sort, + child: MenuRow(text: 'Sort...', icon: AIcons.sort), ), - if (collection.isBrowsing) ...[ - if (AvesApp.mode == AppMode.main) - if (kDebugMode) + if (collection.sortFactor == EntrySortFactor.date) + PopupMenuItem( + key: Key('menu-group'), + value: CollectionAction.group, + child: MenuRow(text: 'Group...', icon: AIcons.group), + ), + if (collection.isBrowsing) ...[ + if (AvesApp.mode == AppMode.main) + if (kDebugMode) + PopupMenuItem( + value: CollectionAction.refresh, + child: MenuRow(text: 'Refresh', icon: AIcons.refresh), + ), + PopupMenuItem( + value: CollectionAction.select, + child: MenuRow(text: 'Select', icon: AIcons.select), + ), + PopupMenuItem( + value: CollectionAction.stats, + child: MenuRow(text: 'Stats', icon: AIcons.stats), + ), + if (canAddShortcuts) PopupMenuItem( - value: CollectionAction.refresh, - child: MenuRow(text: 'Refresh', icon: AIcons.refresh), + value: CollectionAction.addShortcut, + child: MenuRow(text: 'Add shortcut', icon: AIcons.addShortcut), ), - PopupMenuItem( - value: CollectionAction.select, - child: MenuRow(text: 'Select', icon: AIcons.select), - ), - PopupMenuItem( - value: CollectionAction.stats, - child: MenuRow(text: 'Stats', icon: AIcons.stats), - ), - ], - if (collection.isSelecting) ...[ - PopupMenuDivider(), - PopupMenuItem( - value: CollectionAction.copy, - enabled: hasSelection, - child: MenuRow(text: 'Copy to album'), - ), - PopupMenuItem( - value: CollectionAction.move, - enabled: hasSelection, - child: MenuRow(text: 'Move to album'), - ), - PopupMenuItem( - value: CollectionAction.refreshMetadata, - enabled: hasSelection, - child: MenuRow(text: 'Refresh metadata'), - ), - PopupMenuDivider(), - PopupMenuItem( - value: CollectionAction.selectAll, - child: MenuRow(text: 'Select all'), - ), - PopupMenuItem( - value: CollectionAction.selectNone, - enabled: hasSelection, - child: MenuRow(text: 'Select none'), - ), - ] - ]; - }, - onSelected: _onCollectionActionSelected, - ), + ], + if (collection.isSelecting) ...[ + PopupMenuDivider(), + PopupMenuItem( + value: CollectionAction.copy, + enabled: hasSelection, + child: MenuRow(text: 'Copy to album'), + ), + PopupMenuItem( + value: CollectionAction.move, + enabled: hasSelection, + child: MenuRow(text: 'Move to album'), + ), + PopupMenuItem( + value: CollectionAction.refreshMetadata, + enabled: hasSelection, + child: MenuRow(text: 'Refresh metadata'), + ), + PopupMenuDivider(), + PopupMenuItem( + value: CollectionAction.selectAll, + child: MenuRow(text: 'Select all'), + ), + PopupMenuItem( + value: CollectionAction.selectNone, + enabled: hasSelection, + child: MenuRow(text: 'Select none'), + ), + ] + ]; + }, + onSelected: _onCollectionActionSelected, + ); + }, ), ]; } @@ -297,6 +310,9 @@ class _CollectionAppBarState extends State with SingleTickerPr case CollectionAction.stats: _goToStats(); break; + case CollectionAction.addShortcut: + unawaited(AppShortcutService.pin('Collection', collection.filters)); + break; case CollectionAction.group: final value = await showDialog( context: context, @@ -360,16 +376,3 @@ class _CollectionAppBarState extends State with SingleTickerPr ); } } - -enum CollectionAction { - copy, - group, - move, - refresh, - refreshMetadata, - select, - selectAll, - selectNone, - sort, - stats, -} diff --git a/lib/widgets/collection/collection_actions.dart b/lib/widgets/collection/collection_actions.dart new file mode 100644 index 000000000..c71ba093c --- /dev/null +++ b/lib/widgets/collection/collection_actions.dart @@ -0,0 +1,13 @@ +enum CollectionAction { + addShortcut, + copy, + group, + move, + refresh, + refreshMetadata, + select, + selectAll, + selectNone, + sort, + stats, +} diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index d7a4b286e..4f5fd8d9a 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -9,7 +9,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/collection/app_bar.dart'; +import 'package:aves/widgets/collection/collection_actions.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; diff --git a/lib/widgets/common/entry_actions.dart b/lib/widgets/common/entry_actions.dart index 2e9e93a67..47500e61b 100644 --- a/lib/widgets/common/entry_actions.dart +++ b/lib/widgets/common/entry_actions.dart @@ -1,7 +1,21 @@ import 'package:aves/widgets/common/icons.dart'; import 'package:flutter/material.dart'; -enum EntryAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share, toggleFavourite, debug } +enum EntryAction { + delete, + edit, + info, + open, + openMap, + print, + rename, + rotateCCW, + rotateCW, + setAs, + share, + toggleFavourite, + debug, +} class EntryActions { static const selection = [ diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index 42d2f9f2c..6f7d2b47c 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -24,6 +24,7 @@ class AIcons { static const IconData tag = OMIcons.localOffer; // actions + static const IconData addShortcut = OMIcons.bookmarkBorder; static const IconData clear = OMIcons.clear; static const IconData collapse = OMIcons.expandLess; static const IconData createAlbum = OMIcons.addCircleOutline;