viewer: add shortcut action
This commit is contained in:
parent
fe8948ef5e
commit
b2a9a33015
9 changed files with 121 additions and 68 deletions
|
@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Viewer: action to add shortcut to media item
|
||||
|
||||
### Fixed
|
||||
|
||||
- video playback was not using hardware-accelerated codecs on recent devices
|
||||
|
||||
## [v1.5.5] - 2021-11-08
|
||||
|
||||
### Added
|
||||
|
|
|
@ -329,7 +329,8 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val label = call.argument<String>("label")
|
||||
val iconBytes = call.argument<ByteArray>("iconBytes")
|
||||
val filters = call.argument<List<String>>("filters")
|
||||
if (label == null || filters == null) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (label == null || (filters == null && uri == null)) {
|
||||
result.error("pin-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
@ -356,12 +357,19 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
icon = IconCompat.createWithResource(context, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection)
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
|
||||
val intent = when {
|
||||
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
|
||||
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
|
||||
.putExtra("page", "/collection")
|
||||
.putExtra("filters", filters.toTypedArray())
|
||||
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
|
||||
// so we use a joined `String` as fallback
|
||||
.putExtra("filtersString", filters.joinToString(MainActivity.EXTRA_STRING_ARRAY_SEPARATOR))
|
||||
else -> {
|
||||
result.error("pin-intent", "failed to build intent", null)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// multiple shortcuts sharing the same ID cannot be created with different labels or icons
|
||||
// so we provide a unique ID for each one, and let the user manage duplicates (i.e. same filter set), if any
|
||||
|
|
|
@ -4,6 +4,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum EntryAction {
|
||||
addShortcut,
|
||||
copyToClipboard,
|
||||
delete,
|
||||
export,
|
||||
info,
|
||||
|
@ -20,7 +22,6 @@ enum EntryAction {
|
|||
// motion photo,
|
||||
viewMotionPhotoVideo,
|
||||
// external
|
||||
copyToClipboard,
|
||||
edit,
|
||||
open,
|
||||
openMap,
|
||||
|
@ -39,6 +40,7 @@ class EntryActions {
|
|||
EntryAction.delete,
|
||||
EntryAction.rename,
|
||||
EntryAction.export,
|
||||
EntryAction.addShortcut,
|
||||
EntryAction.copyToClipboard,
|
||||
EntryAction.print,
|
||||
EntryAction.viewSource,
|
||||
|
@ -63,9 +65,8 @@ class EntryActions {
|
|||
extension ExtraEntryAction on EntryAction {
|
||||
String getText(BuildContext context) {
|
||||
switch (this) {
|
||||
case EntryAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return context.l10n.entryActionAddFavourite;
|
||||
case EntryAction.addShortcut:
|
||||
return context.l10n.collectionActionAddShortcut;
|
||||
case EntryAction.copyToClipboard:
|
||||
return context.l10n.entryActionCopyToClipboard;
|
||||
case EntryAction.delete:
|
||||
|
@ -74,12 +75,15 @@ extension ExtraEntryAction on EntryAction {
|
|||
return context.l10n.entryActionExport;
|
||||
case EntryAction.info:
|
||||
return context.l10n.entryActionInfo;
|
||||
case EntryAction.rename:
|
||||
return context.l10n.entryActionRename;
|
||||
case EntryAction.print:
|
||||
return context.l10n.entryActionPrint;
|
||||
case EntryAction.rename:
|
||||
return context.l10n.entryActionRename;
|
||||
case EntryAction.share:
|
||||
return context.l10n.entryActionShare;
|
||||
case EntryAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return context.l10n.entryActionAddFavourite;
|
||||
// raster
|
||||
case EntryAction.rotateCCW:
|
||||
return context.l10n.entryActionRotateCCW;
|
||||
|
@ -98,10 +102,10 @@ extension ExtraEntryAction on EntryAction {
|
|||
return context.l10n.entryActionEdit;
|
||||
case EntryAction.open:
|
||||
return context.l10n.entryActionOpen;
|
||||
case EntryAction.setAs:
|
||||
return context.l10n.entryActionSetAs;
|
||||
case EntryAction.openMap:
|
||||
return context.l10n.entryActionOpenMap;
|
||||
case EntryAction.setAs:
|
||||
return context.l10n.entryActionSetAs;
|
||||
// platform
|
||||
case EntryAction.rotateScreen:
|
||||
return context.l10n.entryActionRotateScreen;
|
||||
|
@ -129,9 +133,8 @@ extension ExtraEntryAction on EntryAction {
|
|||
|
||||
IconData? getIconData() {
|
||||
switch (this) {
|
||||
case EntryAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return AIcons.favourite;
|
||||
case EntryAction.addShortcut:
|
||||
return AIcons.addShortcut;
|
||||
case EntryAction.copyToClipboard:
|
||||
return AIcons.clipboard;
|
||||
case EntryAction.delete:
|
||||
|
@ -140,12 +143,15 @@ extension ExtraEntryAction on EntryAction {
|
|||
return AIcons.saveAs;
|
||||
case EntryAction.info:
|
||||
return AIcons.info;
|
||||
case EntryAction.rename:
|
||||
return AIcons.rename;
|
||||
case EntryAction.print:
|
||||
return AIcons.print;
|
||||
case EntryAction.rename:
|
||||
return AIcons.rename;
|
||||
case EntryAction.share:
|
||||
return AIcons.share;
|
||||
case EntryAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return AIcons.favourite;
|
||||
// raster
|
||||
case EntryAction.rotateCCW:
|
||||
return AIcons.rotateLeft;
|
||||
|
@ -162,8 +168,8 @@ extension ExtraEntryAction on EntryAction {
|
|||
// external
|
||||
case EntryAction.edit:
|
||||
case EntryAction.open:
|
||||
case EntryAction.setAs:
|
||||
case EntryAction.openMap:
|
||||
case EntryAction.setAs:
|
||||
return null;
|
||||
// platform
|
||||
case EntryAction.rotateScreen:
|
||||
|
|
|
@ -31,7 +31,7 @@ abstract class AndroidAppService {
|
|||
|
||||
Future<bool> canPinToHomeScreen();
|
||||
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? entry, Set<CollectionFilter> filters);
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri});
|
||||
}
|
||||
|
||||
class PlatformAndroidAppService implements AndroidAppService {
|
||||
|
@ -194,17 +194,17 @@ class PlatformAndroidAppService implements AndroidAppService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? entry, Set<CollectionFilter> filters) async {
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}) async {
|
||||
Uint8List? iconBytes;
|
||||
if (entry != null) {
|
||||
final size = entry.isVideo ? 0.0 : 256.0;
|
||||
if (coverEntry != null) {
|
||||
final size = coverEntry.isVideo ? 0.0 : 256.0;
|
||||
iconBytes = await mediaFileService.getThumbnail(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
pageId: entry.pageId,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
dateModifiedSecs: entry.dateModifiedSecs,
|
||||
uri: coverEntry.uri,
|
||||
mimeType: coverEntry.mimeType,
|
||||
pageId: coverEntry.pageId,
|
||||
rotationDegrees: coverEntry.rotationDegrees,
|
||||
isFlipped: coverEntry.isFlipped,
|
||||
dateModifiedSecs: coverEntry.dateModifiedSecs,
|
||||
extent: size,
|
||||
);
|
||||
}
|
||||
|
@ -212,7 +212,8 @@ class PlatformAndroidAppService implements AndroidAppService {
|
|||
await platform.invokeMethod('pin', <String, dynamic>{
|
||||
'label': label,
|
||||
'iconBytes': iconBytes,
|
||||
'filters': filters.map((filter) => filter.toJson()).toList(),
|
||||
'filters': filters?.map((filter) => filter.toJson()).toList(),
|
||||
'uri': uri,
|
||||
});
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
|
|
|
@ -586,8 +586,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
final result = await showDialog<Tuple2<AvesEntry?, String>>(
|
||||
context: context,
|
||||
builder: (context) => AddShortcutDialog(
|
||||
collection: collection,
|
||||
defaultName: defaultName ?? '',
|
||||
collection: collection,
|
||||
),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
@ -596,6 +596,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
final name = result.item2;
|
||||
if (name.isEmpty) return;
|
||||
|
||||
unawaited(androidAppService.pinToHomeScreen(name, coverEntry, filters));
|
||||
unawaited(androidAppService.pinToHomeScreen(name, coverEntry, filters: filters));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
|
@ -14,13 +13,13 @@ import 'package:tuple/tuple.dart';
|
|||
import 'aves_dialog.dart';
|
||||
|
||||
class AddShortcutDialog extends StatefulWidget {
|
||||
final CollectionLens collection;
|
||||
final CollectionLens? collection;
|
||||
final String defaultName;
|
||||
|
||||
const AddShortcutDialog({
|
||||
Key? key,
|
||||
required this.collection,
|
||||
required this.defaultName,
|
||||
this.collection,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -32,18 +31,17 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
AvesEntry? _coverEntry;
|
||||
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
Set<CollectionFilter> get filters => collection.filters;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final entries = collection.sortedEntries;
|
||||
final _collection = widget.collection;
|
||||
if (_collection != null) {
|
||||
final entries = _collection.sortedEntries;
|
||||
if (entries.isNotEmpty) {
|
||||
final coverEntries = filters.map(covers.coverContentId).whereNotNull().map((id) => entries.firstWhereOrNull((entry) => entry.contentId == id)).whereNotNull();
|
||||
final coverEntries = _collection.filters.map(covers.coverContentId).whereNotNull().map((id) => entries.firstWhereOrNull((entry) => entry.contentId == id)).whereNotNull();
|
||||
_coverEntry = coverEntries.firstOrNull ?? entries.first;
|
||||
}
|
||||
}
|
||||
_nameController.text = widget.defaultName;
|
||||
_validate();
|
||||
}
|
||||
|
@ -123,14 +121,17 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
}
|
||||
|
||||
Future<void> _pickEntry() async {
|
||||
final _collection = widget.collection;
|
||||
if (_collection == null) return;
|
||||
|
||||
final entry = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||
builder: (context) => ItemPickDialog(
|
||||
collection: CollectionLens(
|
||||
source: collection.source,
|
||||
filters: filters,
|
||||
source: _collection.source,
|
||||
filters: _collection.filters,
|
||||
),
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
|
|
|
@ -36,6 +36,7 @@ class ViewerActionEditorPage extends StatelessWidget {
|
|||
EntryAction.delete,
|
||||
EntryAction.rename,
|
||||
EntryAction.export,
|
||||
EntryAction.addShortcut,
|
||||
EntryAction.copyToClipboard,
|
||||
EntryAction.print,
|
||||
EntryAction.rotateScreen,
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
|||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/size_aware.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_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/export_entry_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/rename_entry_dialog.dart';
|
||||
|
@ -30,12 +31,13 @@ import 'package:aves/widgets/viewer/source_viewer_page.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
void onActionSelected(BuildContext context, AvesEntry entry, EntryAction action) {
|
||||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
entry.toggleFavourite();
|
||||
case EntryAction.addShortcut:
|
||||
_addShortcut(context, entry);
|
||||
break;
|
||||
case EntryAction.copyToClipboard:
|
||||
androidAppService.copyToClipboard(entry.uri, entry.bestTitle).then((success) {
|
||||
|
@ -43,10 +45,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
});
|
||||
break;
|
||||
case EntryAction.delete:
|
||||
_showDeleteDialog(context, entry);
|
||||
_delete(context, entry);
|
||||
break;
|
||||
case EntryAction.export:
|
||||
_showExportDialog(context, entry);
|
||||
_export(context, entry);
|
||||
break;
|
||||
case EntryAction.info:
|
||||
ShowInfoNotification().dispatch(context);
|
||||
|
@ -55,8 +57,17 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
EntryPrinter(entry).print(context);
|
||||
break;
|
||||
case EntryAction.rename:
|
||||
_showRenameDialog(context, entry);
|
||||
_rename(context, entry);
|
||||
break;
|
||||
case EntryAction.share:
|
||||
androidAppService.shareEntries({entry}).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
case EntryAction.toggleFavourite:
|
||||
entry.toggleFavourite();
|
||||
break;
|
||||
// raster
|
||||
case EntryAction.rotateCCW:
|
||||
_rotate(context, entry, clockwise: false);
|
||||
break;
|
||||
|
@ -66,6 +77,15 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
case EntryAction.flip:
|
||||
_flip(context, entry);
|
||||
break;
|
||||
// vector
|
||||
case EntryAction.viewSource:
|
||||
_goToSourceViewer(context, entry);
|
||||
break;
|
||||
// motion photo
|
||||
case EntryAction.viewMotionPhotoVideo:
|
||||
OpenEmbeddedDataNotification.motionPhotoVideo().dispatch(context);
|
||||
break;
|
||||
// external
|
||||
case EntryAction.edit:
|
||||
androidAppService.edit(entry.uri, entry.mimeType).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
|
@ -81,31 +101,37 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
case EntryAction.rotateScreen:
|
||||
_rotateScreen(context);
|
||||
break;
|
||||
case EntryAction.setAs:
|
||||
androidAppService.setAs(entry.uri, entry.mimeType).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
case EntryAction.share:
|
||||
androidAppService.shareEntries({entry}).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
case EntryAction.viewSource:
|
||||
_goToSourceViewer(context, entry);
|
||||
break;
|
||||
case EntryAction.viewMotionPhotoVideo:
|
||||
OpenEmbeddedDataNotification.motionPhotoVideo().dispatch(context);
|
||||
// platform
|
||||
case EntryAction.rotateScreen:
|
||||
_rotateScreen(context);
|
||||
break;
|
||||
// debug
|
||||
case EntryAction.debug:
|
||||
_goToDebug(context, entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addShortcut(BuildContext context, AvesEntry entry) async {
|
||||
final result = await showDialog<Tuple2<AvesEntry?, String>>(
|
||||
context: context,
|
||||
builder: (context) => AddShortcutDialog(
|
||||
defaultName: entry.bestTitle ?? '',
|
||||
),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
final name = result.item2;
|
||||
if (name.isEmpty) return;
|
||||
|
||||
unawaited(androidAppService.pinToHomeScreen(name, entry, uri: entry.uri));
|
||||
}
|
||||
|
||||
Future<void> _flip(BuildContext context, AvesEntry entry) async {
|
||||
if (!await checkStoragePermission(context, {entry})) return;
|
||||
|
||||
|
@ -131,7 +157,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _showDeleteDialog(BuildContext context, AvesEntry entry) async {
|
||||
Future<void> _delete(BuildContext context, AvesEntry entry) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
|
@ -166,7 +192,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _showExportDialog(BuildContext context, AvesEntry entry) async {
|
||||
Future<void> _export(BuildContext context, AvesEntry entry) async {
|
||||
final source = context.read<CollectionSource>();
|
||||
if (!source.initialized) {
|
||||
await source.init();
|
||||
|
@ -273,7 +299,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _showRenameDialog(BuildContext context, AvesEntry entry) async {
|
||||
Future<void> _rename(BuildContext context, AvesEntry entry) async {
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => RenameEntryDialog(entry: entry),
|
||||
|
|
|
@ -88,6 +88,7 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
return targetEntry.isMotionPhoto;
|
||||
case EntryAction.rotateScreen:
|
||||
return settings.isRotationLocked;
|
||||
case EntryAction.addShortcut:
|
||||
case EntryAction.copyToClipboard:
|
||||
case EntryAction.edit:
|
||||
case EntryAction.info:
|
||||
|
@ -208,6 +209,7 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
case EntryAction.addShortcut:
|
||||
case EntryAction.copyToClipboard:
|
||||
case EntryAction.delete:
|
||||
case EntryAction.export:
|
||||
|
|
Loading…
Reference in a new issue