From 9208d66e22d0bea7a0277fc7c095f8f1ab5343c9 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 6 Dec 2022 18:22:52 +0100 Subject: [PATCH] #434 share quick action to share parts of motion photo --- CHANGELOG.md | 1 + .../aves/channel/calls/EmbeddedDataHandler.kt | 26 +++++++- lib/l10n/app_en.arb | 2 + lib/model/actions/share_actions.dart | 27 +++++++++ lib/services/media/embedded_data_service.dart | 18 ++++++ lib/widgets/about/bug_report.dart | 2 +- lib/widgets/about/credits.dart | 2 +- lib/widgets/about/licenses.dart | 2 +- lib/widgets/about/translators.dart | 2 +- .../app_bar/quick_choosers/album_chooser.dart | 7 ++- .../button.dart} | 17 +++--- .../{filter_chooser.dart => common/menu.dart} | 26 ++++---- .../{ => common}/quick_chooser.dart | 0 .../{ => common}/route_layout.dart | 0 .../{ => quick_choosers}/move_button.dart | 6 +- .../{ => quick_choosers}/rate_button.dart | 2 +- .../app_bar/quick_choosers/rate_chooser.dart | 2 +- .../app_bar/quick_choosers/share_button.dart | 60 +++++++++++++++++++ .../app_bar/quick_choosers/share_chooser.dart | 47 +++++++++++++++ .../{ => quick_choosers}/tag_button.dart | 6 +- .../app_bar/quick_choosers/tag_chooser.dart | 7 ++- lib/widgets/common/basic/menu.dart | 5 +- .../cover_selection_dialog.dart | 8 +-- lib/widgets/dialogs/tile_view_dialog.dart | 4 +- .../viewer/action/entry_action_delegate.dart | 28 +++++++++ lib/widgets/viewer/info/basic_section.dart | 4 +- .../viewer/overlay/video/progress_bar.dart | 4 +- .../viewer/overlay/viewer_buttons.dart | 15 ++++- untranslated.json | 44 ++++++++++++++ 29 files changed, 316 insertions(+), 58 deletions(-) create mode 100644 lib/model/actions/share_actions.dart rename lib/widgets/common/app_bar/quick_choosers/{chooser_button.dart => common/button.dart} (88%) rename lib/widgets/common/app_bar/quick_choosers/{filter_chooser.dart => common/menu.dart} (87%) rename lib/widgets/common/app_bar/quick_choosers/{ => common}/quick_chooser.dart (100%) rename lib/widgets/common/app_bar/quick_choosers/{ => common}/route_layout.dart (100%) rename lib/widgets/common/app_bar/{ => quick_choosers}/move_button.dart (90%) rename lib/widgets/common/app_bar/{ => quick_choosers}/rate_button.dart (93%) create mode 100644 lib/widgets/common/app_bar/quick_choosers/share_button.dart create mode 100644 lib/widgets/common/app_bar/quick_choosers/share_chooser.dart rename lib/widgets/common/app_bar/{ => quick_choosers}/tag_button.dart (90%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5846b78d4..d98c7849b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Viewer: optionally show rating & tags on overlay - Viewer: long press on copy/move/rating/tag quick action for quicker action +- Viewer: long press on share quick action to share parts of motion photo - Search: missing address, portrait, landscape filters - Map: edit cluster location - Lithuanian translation (thanks Gediminas Murauskas) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index fe779c968..62f13accd 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -46,6 +46,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) } + "extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) } "extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) } "extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) } "extractXmpDataProp" -> ioScope.launch { safe(call, result, ::extractXmpDataProp) } @@ -83,6 +84,27 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { result.success(thumbnails) } + private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) { + val mimeType = call.argument("mimeType") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val sizeBytes = call.argument("sizeBytes")?.toLong() + val displayName = call.argument("displayName") + if (mimeType == null || uri == null || sizeBytes == null) { + result.error("extractMotionPhotoImage-args", "missing arguments", null) + return + } + + MultiPage.getMotionPhotoOffset(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes -> + val imageSizeBytes = sizeBytes - videoSizeBytes + StorageUtils.openInputStream(context, uri)?.let { input -> + copyEmbeddedBytes(result, mimeType, displayName, input, imageSizeBytes) + } + return + } + + result.error("extractMotionPhotoImage-empty", "failed to extract image from motion photo at uri=$uri", null) + } + private fun extractMotionPhotoVideo(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } @@ -166,9 +188,9 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { try { val embedBytes: ByteArray = if (props.size == 1) { val prop = props.first() as XMPPropName - xmpDirs.mapNotNull { it.xmpMeta.getPropertyBase64(prop.nsUri, prop.toString()) }.first() + xmpDirs.firstNotNullOf { it.xmpMeta.getPropertyBase64(prop.nsUri, prop.toString()) } } else { - xmpDirs.mapNotNull { it.xmpMeta.getSafeStructField(props) }.first().let { + xmpDirs.firstNotNullOf { it.xmpMeta.getSafeStructField(props) }.let { XMPUtils.decodeBase64(it.value) } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 12973db06..6aa393539 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -90,6 +90,8 @@ "entryActionFlip": "Flip horizontally", "entryActionPrint": "Print", "entryActionShare": "Share", + "entryActionShareImageOnly": "Share image only", + "entryActionShareVideoOnly": "Share video only", "entryActionViewSource": "View source", "entryActionShowGeoTiffOnMap": "Show as map overlay", "entryActionConvertMotionPhotoToStillImage": "Convert to still image", diff --git a/lib/model/actions/share_actions.dart b/lib/model/actions/share_actions.dart new file mode 100644 index 000000000..bd3480e64 --- /dev/null +++ b/lib/model/actions/share_actions.dart @@ -0,0 +1,27 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +enum ShareAction { imageOnly, videoOnly, } + +extension ExtraShareAction on ShareAction { + String getText(BuildContext context) { + switch (this) { + case ShareAction.imageOnly: + return context.l10n.entryActionShareImageOnly; + case ShareAction.videoOnly: + return context.l10n.entryActionShareVideoOnly; + } + } + + Widget getIcon() => Icon(_getIconData()); + + IconData _getIconData() { + switch (this) { + case ShareAction.imageOnly: + return AIcons.image; + case ShareAction.videoOnly: + return AIcons.video; + } + } +} \ No newline at end of file diff --git a/lib/services/media/embedded_data_service.dart b/lib/services/media/embedded_data_service.dart index b1c85159f..23b47849c 100644 --- a/lib/services/media/embedded_data_service.dart +++ b/lib/services/media/embedded_data_service.dart @@ -6,6 +6,8 @@ import 'package:flutter/services.dart'; abstract class EmbeddedDataService { Future> getExifThumbnails(AvesEntry entry); + Future extractMotionPhotoImage(AvesEntry entry); + Future extractMotionPhotoVideo(AvesEntry entry); Future extractVideoEmbeddedPicture(AvesEntry entry); @@ -31,6 +33,22 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { return []; } + @override + Future extractMotionPhotoImage(AvesEntry entry) async { + try { + final result = await _platform.invokeMethod('extractMotionPhotoImage', { + 'mimeType': entry.mimeType, + 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, + 'displayName': ['${entry.bestTitle}', 'Image'].join(Constants.separator), + }); + if (result != null) return result as Map; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return {}; + } + @override Future extractMotionPhotoVideo(AvesEntry entry) async { try { diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index fcb11f6b5..90e97218a 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -56,7 +56,7 @@ class _BugReportState extends State with FeedbackMixin { children: [ ExpansionPanel( headerBuilder: (context, isExpanded) => ConstrainedBox( - constraints: const BoxConstraints(minHeight: 48), + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), alignment: AlignmentDirectional.centerStart, diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart index 4a5c6e059..e4422cce3 100644 --- a/lib/widgets/about/credits.dart +++ b/lib/widgets/about/credits.dart @@ -15,7 +15,7 @@ class AboutCredits extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( - constraints: const BoxConstraints(minHeight: 48), + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), child: Align( alignment: AlignmentDirectional.centerStart, child: Text(l10n.aboutCreditsSectionTitle, style: Constants.knownTitleTextStyle), diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index d317996d9..405447606 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -103,7 +103,7 @@ class _LicensesState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( - constraints: const BoxConstraints(minHeight: 48), + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), child: Align( alignment: AlignmentDirectional.centerStart, child: Text(context.l10n.aboutLicensesSectionTitle, style: Constants.knownTitleTextStyle), diff --git a/lib/widgets/about/translators.dart b/lib/widgets/about/translators.dart index 08b350222..3a5040b04 100644 --- a/lib/widgets/about/translators.dart +++ b/lib/widgets/about/translators.dart @@ -53,7 +53,7 @@ class AboutTranslators extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( - constraints: const BoxConstraints(minHeight: 48), + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), child: Align( alignment: AlignmentDirectional.centerStart, child: Text(l10n.aboutTranslatorsSectionTitle, style: Constants.knownTitleTextStyle), diff --git a/lib/widgets/common/app_bar/quick_choosers/album_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/album_chooser.dart index 9dfaecb00..90658ff4f 100644 --- a/lib/widgets/common/app_bar/quick_choosers/album_chooser.dart +++ b/lib/widgets/common/app_bar/quick_choosers/album_chooser.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -26,13 +26,14 @@ class AlbumQuickChooser extends StatelessWidget { @override Widget build(BuildContext context) { final source = context.read(); - return FilterQuickChooser( + return MenuQuickChooser( valueNotifier: valueNotifier, options: options, + autoReverse: true, blurred: blurred, chooserPosition: chooserPosition, pointerGlobalPosition: pointerGlobalPosition, - buildFilterChip: (context, album) => AvesFilterChip( + itemBuilder: (context, album) => AvesFilterChip( filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), showGenericIcon: false, ), diff --git a/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart b/lib/widgets/common/app_bar/quick_choosers/common/button.dart similarity index 88% rename from lib/widgets/common/app_bar/quick_choosers/chooser_button.dart rename to lib/widgets/common/app_bar/quick_choosers/common/button.dart index a8fff4e51..2d1cf1603 100644 --- a/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart +++ b/lib/widgets/common/app_bar/quick_choosers/common/button.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/route_layout.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/route_layout.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -35,6 +35,8 @@ abstract class ChooserQuickButtonState, U> exten Curve get animationCurve => Curves.easeOutQuad; + bool get hasChooser => widget.onChooserValue != null; + Widget buildChooser(Animation animation, PopupMenuPosition chooserPosition); ValueNotifier get chooserValueNotifier => _chooserValueNotifier; @@ -50,19 +52,18 @@ abstract class ChooserQuickButtonState, U> exten @override Widget build(BuildContext context) { - final onChooserValue = widget.onChooserValue; - final isChooserEnabled = onChooserValue != null; + final _hasChooser = hasChooser; return GestureDetector( behavior: HitTestBehavior.opaque, - onLongPressStart: isChooserEnabled ? _showChooser : null, - onLongPressMoveUpdate: isChooserEnabled ? _moveUpdateStreamController.add : null, - onLongPressEnd: isChooserEnabled + onLongPressStart: _hasChooser ? _showChooser : null, + onLongPressMoveUpdate: _hasChooser ? _moveUpdateStreamController.add : null, + onLongPressEnd: _hasChooser ? (details) { _clearChooserOverlayEntry(); final selectedValue = _chooserValueNotifier.value; if (selectedValue != null) { - onChooserValue(selectedValue); + widget.onChooserValue?.call(selectedValue); } } : null, @@ -70,7 +71,7 @@ abstract class ChooserQuickButtonState, U> exten child: IconButton( icon: icon, onPressed: widget.onPressed, - tooltip: isChooserEnabled ? null : tooltip, + tooltip: _hasChooser ? null : tooltip, ), ); } diff --git a/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/common/menu.dart similarity index 87% rename from lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart rename to lib/widgets/common/app_bar/quick_choosers/common/menu.dart index 8128dd2b0..cef81ee4f 100644 --- a/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart +++ b/lib/widgets/common/app_bar/quick_choosers/common/menu.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/quick_chooser.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/quick_chooser.dart'; import 'package:aves_ui/aves_ui.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -10,31 +10,33 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; -class FilterQuickChooser extends StatefulWidget { +class MenuQuickChooser extends StatefulWidget { final ValueNotifier valueNotifier; final List options; + final bool autoReverse; final bool blurred; final PopupMenuPosition chooserPosition; final Stream pointerGlobalPosition; - final Widget Function(BuildContext context, T album) buildFilterChip; + final Widget Function(BuildContext context, T menuItem) itemBuilder; static const int maxOptionCount = 5; - FilterQuickChooser({ + MenuQuickChooser({ super.key, required this.valueNotifier, required List options, + required this.autoReverse, required this.blurred, required this.chooserPosition, required this.pointerGlobalPosition, - required this.buildFilterChip, + required this.itemBuilder, }) : options = options.take(maxOptionCount).toList(); @override - State> createState() => _FilterQuickChooserState(); + State> createState() => _MenuQuickChooserState(); } -class _FilterQuickChooserState extends State> { +class _MenuQuickChooserState extends State> { final List _subscriptions = []; final ValueNotifier _selectedRowRect = ValueNotifier(Rect.zero); @@ -42,7 +44,7 @@ class _FilterQuickChooserState extends State> { List get options => widget.options; - bool get reversed => widget.chooserPosition == PopupMenuPosition.over; + bool get reversed => widget.autoReverse && widget.chooserPosition == PopupMenuPosition.over; static const double intraPadding = 8; @@ -54,7 +56,7 @@ class _FilterQuickChooserState extends State> { } @override - void didUpdateWidget(covariant FilterQuickChooser oldWidget) { + void didUpdateWidget(covariant MenuQuickChooser oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -66,11 +68,11 @@ class _FilterQuickChooserState extends State> { super.dispose(); } - void _registerWidget(FilterQuickChooser widget) { + void _registerWidget(MenuQuickChooser widget) { _subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove)); } - void _unregisterWidget(FilterQuickChooser widget) { + void _unregisterWidget(MenuQuickChooser widget) { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -89,7 +91,7 @@ class _FilterQuickChooserState extends State> { final isFirst = index == (reversed ? options.length - 1 : 0); return Padding( padding: EdgeInsets.only(top: isFirst ? intraPadding : 0, bottom: intraPadding), - child: widget.buildFilterChip(context, value), + child: widget.itemBuilder(context, value), ); }).toList(); diff --git a/lib/widgets/common/app_bar/quick_choosers/quick_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/common/quick_chooser.dart similarity index 100% rename from lib/widgets/common/app_bar/quick_choosers/quick_chooser.dart rename to lib/widgets/common/app_bar/quick_choosers/common/quick_chooser.dart diff --git a/lib/widgets/common/app_bar/quick_choosers/route_layout.dart b/lib/widgets/common/app_bar/quick_choosers/common/route_layout.dart similarity index 100% rename from lib/widgets/common/app_bar/quick_choosers/route_layout.dart rename to lib/widgets/common/app_bar/quick_choosers/common/route_layout.dart diff --git a/lib/widgets/common/app_bar/move_button.dart b/lib/widgets/common/app_bar/quick_choosers/move_button.dart similarity index 90% rename from lib/widgets/common/app_bar/move_button.dart rename to lib/widgets/common/app_bar/quick_choosers/move_button.dart index 2d224699c..5a2a47768 100644 --- a/lib/widgets/common/app_bar/move_button.dart +++ b/lib/widgets/common/app_bar/quick_choosers/move_button.dart @@ -4,8 +4,8 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/common/app_bar/quick_choosers/album_chooser.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/chooser_button.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:collection/collection.dart'; @@ -39,7 +39,7 @@ class _MoveButtonState extends ChooserQuickButtonState { @override Widget buildChooser(Animation animation, PopupMenuPosition chooserPosition) { final options = settings.recentDestinationAlbums; - final takeCount = FilterQuickChooser.maxOptionCount - options.length; + final takeCount = MenuQuickChooser.maxOptionCount - options.length; if (takeCount > 0) { final source = context.read(); final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet(); diff --git a/lib/widgets/common/app_bar/rate_button.dart b/lib/widgets/common/app_bar/quick_choosers/rate_button.dart similarity index 93% rename from lib/widgets/common/app_bar/rate_button.dart rename to lib/widgets/common/app_bar/quick_choosers/rate_button.dart index c31e718b2..9d51218f6 100644 --- a/lib/widgets/common/app_bar/rate_button.dart +++ b/lib/widgets/common/app_bar/quick_choosers/rate_button.dart @@ -1,5 +1,5 @@ import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/chooser_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/button.dart'; import 'package:aves/widgets/common/app_bar/quick_choosers/rate_chooser.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/app_bar/quick_choosers/rate_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/rate_chooser.dart index 25e8420b2..d28aa90fe 100644 --- a/lib/widgets/common/app_bar/quick_choosers/rate_chooser.dart +++ b/lib/widgets/common/app_bar/quick_choosers/rate_chooser.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/quick_chooser.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/quick_chooser.dart'; import 'package:flutter/material.dart'; class RateQuickChooser extends StatefulWidget { diff --git a/lib/widgets/common/app_bar/quick_choosers/share_button.dart b/lib/widgets/common/app_bar/quick_choosers/share_button.dart new file mode 100644 index 000000000..18218d5a1 --- /dev/null +++ b/lib/widgets/common/app_bar/quick_choosers/share_button.dart @@ -0,0 +1,60 @@ +import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/actions/share_actions.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/share_chooser.dart'; +import 'package:flutter/material.dart'; + +class ShareButton extends ChooserQuickButton { + final Set entries; + + const ShareButton({ + super.key, + required super.blurred, + required this.entries, + super.onChooserValue, + required super.onPressed, + }); + + @override + State createState() => _ShareButtonState(); +} + +class _ShareButtonState extends ChooserQuickButtonState { + EntryAction get action => EntryAction.share; + + @override + Widget get icon => action.getIcon(); + + @override + String get tooltip => action.getText(context); + + @override + bool get hasChooser => super.hasChooser && options.isNotEmpty; + + List get options => [ + if (widget.entries.any((entry) => entry.isMotionPhoto)) ...[ + ShareAction.imageOnly, + ShareAction.videoOnly, + ], + ]; + + @override + Widget buildChooser(Animation animation, PopupMenuPosition chooserPosition) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + alignment: chooserPosition == PopupMenuPosition.over ? Alignment.bottomCenter : Alignment.topCenter, + child: ShareQuickChooser( + valueNotifier: chooserValueNotifier, + options: options, + autoReverse: false, + blurred: widget.blurred, + chooserPosition: chooserPosition, + pointerGlobalPosition: pointerGlobalPosition, + ), + ), + ); + } +} diff --git a/lib/widgets/common/app_bar/quick_choosers/share_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/share_chooser.dart new file mode 100644 index 000000000..47b9da0ab --- /dev/null +++ b/lib/widgets/common/app_bar/quick_choosers/share_chooser.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:aves/model/actions/share_actions.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/menu.dart'; +import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:flutter/material.dart'; + +class ShareQuickChooser extends StatelessWidget { + final ValueNotifier valueNotifier; + final List options; + final bool autoReverse; + final bool blurred; + final PopupMenuPosition chooserPosition; + final Stream pointerGlobalPosition; + + const ShareQuickChooser({ + super.key, + required this.valueNotifier, + required this.options, + required this.autoReverse, + required this.blurred, + required this.chooserPosition, + required this.pointerGlobalPosition, + }); + + @override + Widget build(BuildContext context) { + return MenuQuickChooser( + valueNotifier: valueNotifier, + options: options, + autoReverse: autoReverse, + blurred: blurred, + chooserPosition: chooserPosition, + pointerGlobalPosition: pointerGlobalPosition, + itemBuilder: (context, action) => ConstrainedBox( + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 8), + child: MenuRow( + text: action.getText(context), + icon: action.getIcon(), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common/app_bar/tag_button.dart b/lib/widgets/common/app_bar/quick_choosers/tag_button.dart similarity index 90% rename from lib/widgets/common/app_bar/tag_button.dart rename to lib/widgets/common/app_bar/quick_choosers/tag_button.dart index 897dbdb1f..1360d44ef 100644 --- a/lib/widgets/common/app_bar/tag_button.dart +++ b/lib/widgets/common/app_bar/quick_choosers/tag_button.dart @@ -3,8 +3,8 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/chooser_button.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/app_bar/quick_choosers/tag_chooser.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; @@ -36,7 +36,7 @@ class _TagButtonState extends ChooserQuickButtonState animation, PopupMenuPosition chooserPosition) { final options = settings.recentTags; - final takeCount = FilterQuickChooser.maxOptionCount - options.length; + final takeCount = MenuQuickChooser.maxOptionCount - options.length; if (takeCount > 0) { final source = context.read(); final filters = source.sortedTags.map(TagFilter.new).whereNot(options.contains).toSet(); diff --git a/lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart index 550353623..358faead3 100644 --- a/lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart +++ b/lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; @@ -23,13 +23,14 @@ class TagQuickChooser extends StatelessWidget { @override Widget build(BuildContext context) { - return FilterQuickChooser( + return MenuQuickChooser( valueNotifier: valueNotifier, options: options, + autoReverse: true, blurred: blurred, chooserPosition: chooserPosition, pointerGlobalPosition: pointerGlobalPosition, - buildFilterChip: (context, filter) => AvesFilterChip( + itemBuilder: (context, filter) => AvesFilterChip( filter: filter, showGenericIcon: false, ), diff --git a/lib/widgets/common/basic/menu.dart b/lib/widgets/common/basic/menu.dart index d9b95d2a3..d25e30b83 100644 --- a/lib/widgets/common/basic/menu.dart +++ b/lib/widgets/common/basic/menu.dart @@ -15,6 +15,7 @@ class MenuRow extends StatelessWidget { @override Widget build(BuildContext context) { return Row( + mainAxisSize: MainAxisSize.min, children: [ if (icon != null) Padding( @@ -26,7 +27,9 @@ class MenuRow extends StatelessWidget { child: icon!, ), ), - Expanded(child: Text(text)), + Flexible( + child: Text(text), + ), ], ); } diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index 281aac356..71164c2b2 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -131,9 +131,7 @@ class _CoverSelectionDialogState extends State { ), ), ConstrainedBox( - constraints: BoxConstraints( - maxHeight: maxHeight, - ), + constraints: BoxConstraints(maxHeight: maxHeight), child: TabBarView( physics: const NeverScrollableScrollPhysics(), children: tabs @@ -179,9 +177,7 @@ class _CoverSelectionDialogState extends State { final availableBodyWidth = constraints.maxWidth; final maxWidth = min(availableBodyWidth, tabBodyMaxWidth(context)); return ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxWidth, - ), + constraints: BoxConstraints(maxWidth: maxWidth), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index e00c57d50..d7c577385 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -147,9 +147,7 @@ class _TileViewDialogState extends State> with children: [ const SizedBox(height: 8), ConstrainedBox( - constraints: const BoxConstraints( - minHeight: kMinInteractiveDimension, - ), + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), child: Row( children: [ Icon(icon), diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index c2f6b33cb..1a10c2e79 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/move_type.dart'; +import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_metadata_edition.dart'; @@ -294,6 +295,33 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix ); } + Future quickShare(BuildContext context, ShareAction action) async { + switch (action) { + case ShareAction.imageOnly: + if (mainEntry.isMotionPhoto) { + final fields = await embeddedDataService.extractMotionPhotoImage(mainEntry); + await _shareMotionPhotoPart(context, fields); + } + break; + case ShareAction.videoOnly: + if (mainEntry.isMotionPhoto) { + final fields = await embeddedDataService.extractMotionPhotoVideo(mainEntry); + await _shareMotionPhotoPart(context, fields); + } + break; + } + } + + Future _shareMotionPhotoPart(BuildContext context, Map fields) async { + final uri = fields['uri'] as String?; + final mimeType = fields['mimeType'] as String?; + if (uri != null && mimeType != null) { + await androidAppService.shareSingle(uri, mimeType).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); + } + } + void quickRate(BuildContext context, int rating) { final targetEntry = _getTargetEntry(context, EntryAction.editRating); _metadataActionDelegate.quickRate(context, targetEntry, rating); diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 74951aad4..0839fbe0f 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -16,8 +16,8 @@ import 'package:aves/theme/colors.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; -import 'package:aves/widgets/common/app_bar/rate_button.dart'; -import 'package:aves/widgets/common/app_bar/tag_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/rate_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/tag_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; diff --git a/lib/widgets/viewer/overlay/video/progress_bar.dart b/lib/widgets/viewer/overlay/video/progress_bar.dart index 06d12f9c9..b96e26c10 100644 --- a/lib/widgets/viewer/overlay/video/progress_bar.dart +++ b/lib/widgets/viewer/overlay/video/progress_bar.dart @@ -64,9 +64,7 @@ class _VideoProgressBarState extends State { if (_playingOnDragStart) controller!.play(); }, child: ConstrainedBox( - constraints: const BoxConstraints( - minHeight: kMinInteractiveDimension, - ), + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index d116eeaf7..1290a337d 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -5,9 +5,10 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/app_bar/favourite_toggler.dart'; -import 'package:aves/widgets/common/app_bar/move_button.dart'; -import 'package:aves/widgets/common/app_bar/rate_button.dart'; -import 'package:aves/widgets/common/app_bar/tag_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/move_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/rate_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/share_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/tag_button.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup_menu_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -223,6 +224,14 @@ class ViewerButtonRowContent extends StatelessWidget { onPressed: onPressed, ); break; + case EntryAction.share: + child = ShareButton( + blurred: blurred, + entries: {mainEntry}, + onChooserValue: (action) => _entryActionDelegate.quickShare(context, action), + onPressed: onPressed, + ); + break; case EntryAction.toggleFavourite: child = FavouriteToggler( entries: {favouriteTargetEntry}, diff --git a/untranslated.json b/untranslated.json index 889e296cd..fda86e17e 100644 --- a/untranslated.json +++ b/untranslated.json @@ -56,6 +56,8 @@ "entryActionFlip", "entryActionPrint", "entryActionShare", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryActionViewSource", "entryActionShowGeoTiffOnMap", "entryActionConvertMotionPhotoToStillImage", @@ -595,10 +597,14 @@ ], "de": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation" ], "el": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", @@ -607,6 +613,8 @@ ], "es": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation" ], @@ -667,6 +675,8 @@ "entryActionFlip", "entryActionPrint", "entryActionShare", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryActionViewSource", "entryActionShowGeoTiffOnMap", "entryActionConvertMotionPhotoToStillImage", @@ -1206,10 +1216,14 @@ ], "fr": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation" ], "gl": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", @@ -1672,6 +1686,8 @@ ], "id": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", @@ -1688,6 +1704,8 @@ ], "it": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", @@ -1697,6 +1715,8 @@ "ja": [ "chipActionFilterIn", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", @@ -1713,18 +1733,26 @@ ], "ko": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation" ], "lt": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation" ], "nb": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation" ], "nl": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", @@ -1743,6 +1771,8 @@ "nn": [ "sourceStateLoading", "sourceStateCataloguing", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation", "filterBinLabel", "filterNoLocationLabel", @@ -2191,6 +2221,8 @@ "timeMinutes", "timeDays", "focalLength", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", @@ -2688,6 +2720,8 @@ ], "pt": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", @@ -2704,6 +2738,8 @@ ], "ro": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", @@ -2712,6 +2748,8 @@ ], "ru": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation" ], @@ -2722,6 +2760,8 @@ "timeDays", "focalLength", "applyButtonLabel", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryActionShowGeoTiffOnMap", "videoActionCaptureFrame", "entryInfoActionRemoveLocation", @@ -3091,10 +3131,14 @@ ], "tr": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation" ], "zh": [ + "entryActionShareImageOnly", + "entryActionShareVideoOnly", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel",