shortcuts: pin to filtered collection

This commit is contained in:
Thibault Deckers 2020-09-09 18:57:48 +09:00
parent 96ee072253
commit 89360ffa30
18 changed files with 239 additions and 76 deletions

View file

@ -46,6 +46,7 @@
<application
android:name="io.flutter.app.FlutterApplication"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true">
<activity

View file

@ -19,6 +19,7 @@ import java.util.Objects;
import app.loup.streams_channel.StreamsChannel;
import deckers.thibault.aves.channel.calls.AppAdapterHandler;
import deckers.thibault.aves.channel.calls.AppShortcutHandler;
import deckers.thibault.aves.channel.calls.ImageFileHandler;
import deckers.thibault.aves.channel.calls.MetadataHandler;
import deckers.thibault.aves.channel.calls.StorageHandler;
@ -48,6 +49,7 @@ public class MainActivity extends FlutterActivity {
BinaryMessenger messenger = Objects.requireNonNull(getFlutterEngine()).getDartExecutor().getBinaryMessenger();
new MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(new AppAdapterHandler(this));
new MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(new AppShortcutHandler(this));
new MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(new ImageFileHandler(this));
new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler(this));
new MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(new StorageHandler(this));

View file

@ -0,0 +1,64 @@
package deckers.thibault.aves.channel.calls;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import java.util.List;
import deckers.thibault.aves.MainActivity;
import deckers.thibault.aves.R;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class AppShortcutHandler implements MethodChannel.MethodCallHandler {
public static final String CHANNEL = "deckers.thibault/aves/shortcut";
private Context context;
public AppShortcutHandler(Context context) {
this.context = context;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "canPin": {
result.success(ShortcutManagerCompat.isRequestPinShortcutSupported(context));
break;
}
case "pin": {
String label = call.argument("label");
List<String> filters = call.argument("filters");
pin(label, filters);
result.success(null);
break;
}
default:
result.notImplemented();
break;
}
}
private void pin(String label, @Nullable List<String> 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);
}
}

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#455A64">
<group android:scaleX="1.7226"
android:scaleY="1.7226"
android:translateX="33.3288"
android:translateY="33.3288">
<path
android:fillColor="@android:color/white"
android:pathData="M20,4v12L8,16L8,4h12m0,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM11.5,11.67l1.69,2.26 2.48,-3.1L19,15L9,15zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z"/>
</group>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_shortcut_background"/>
<foreground android:drawable="@drawable/ic_shortcut_collection_foreground"/>
</adaptive-icon>

View file

@ -24,6 +24,7 @@ class AlbumFilter extends CollectionFilter {
json['uniqueName'],
);
@override
Map<String, dynamic> toJson() => {
'type': type,
'album': album,

View file

@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
class FavouriteFilter extends CollectionFilter {
static const type = 'favourite';
@override
Map<String, dynamic> toJson() => {
'type': type,
};

View file

@ -45,6 +45,8 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
const CollectionFilter();
Map<String, dynamic> toJson();
bool filter(ImageEntry entry);
bool get isUnique => true;

View file

@ -23,6 +23,7 @@ class LocationFilter extends CollectionFilter {
json['location'],
);
@override
Map<String, dynamic> toJson() => {
'type': type,
'level': level.toString(),

View file

@ -43,6 +43,7 @@ class MimeFilter extends CollectionFilter {
json['mime'],
);
@override
Map<String, dynamic> toJson() => {
'type': type,
'mime': mime,

View file

@ -37,6 +37,7 @@ class QueryFilter extends CollectionFilter {
json['query'],
);
@override
Map<String, dynamic> toJson() => {
'type': type,
'query': query,

View file

@ -15,6 +15,7 @@ class TagFilter extends CollectionFilter {
json['tag'],
);
@override
Map<String, dynamic> toJson() => {
'type': type,
'tag': tag,

View file

@ -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<bool> 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<void> pin(String label, Set<CollectionFilter> filters) async {
try {
await platform.invokeMethod('pin', <String, dynamic>{
'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}');
}
}
}

View file

@ -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<CollectionAppBar> with SingleTickerPr
final TextEditingController _searchFieldController = TextEditingController();
SelectionActionDelegate _actionDelegate;
AnimationController _browseToSelectAnimation;
Future<bool> _canAddShortcutsLoader;
CollectionLens get collection => widget.collection;
@ -54,6 +57,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
duration: Durations.iconAnimation,
vsync: this,
);
_canAddShortcutsLoader = AppShortcutService.canPin();
_registerWidget(widget);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight());
}
@ -187,71 +191,80 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
);
},
)),
Builder(
builder: (context) => PopupMenuButton<CollectionAction>(
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<bool>(
future: _canAddShortcutsLoader,
builder: (context, snapshot) {
final canAddShortcuts = snapshot.data ?? false;
return PopupMenuButton<CollectionAction>(
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<CollectionAppBar> with SingleTickerPr
case CollectionAction.stats:
_goToStats();
break;
case CollectionAction.addShortcut:
unawaited(AppShortcutService.pin('Collection', collection.filters));
break;
case CollectionAction.group:
final value = await showDialog<EntryGroupFactor>(
context: context,
@ -360,16 +376,3 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
);
}
}
enum CollectionAction {
copy,
group,
move,
refresh,
refreshMetadata,
select,
selectAll,
selectNone,
sort,
stats,
}

View file

@ -0,0 +1,13 @@
enum CollectionAction {
addShortcut,
copy,
group,
move,
refresh,
refreshMetadata,
select,
selectAll,
selectNone,
sort,
stats,
}

View file

@ -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';

View file

@ -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 = [

View file

@ -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;