#434 share quick action to share parts of motion photo

This commit is contained in:
Thibault Deckers 2022-12-06 18:22:52 +01:00
parent b8510e9676
commit 9208d66e22
29 changed files with 316 additions and 58 deletions

View file

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

View file

@ -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<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("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<String>("mimeType")
val uri = call.argument<String>("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)
}
}

View file

@ -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",

View file

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

View file

@ -6,6 +6,8 @@ import 'package:flutter/services.dart';
abstract class EmbeddedDataService {
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry);
Future<Map> extractMotionPhotoImage(AvesEntry entry);
Future<Map> extractMotionPhotoVideo(AvesEntry entry);
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry);
@ -31,6 +33,22 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
return [];
}
@override
Future<Map> extractMotionPhotoImage(AvesEntry entry) async {
try {
final result = await _platform.invokeMethod('extractMotionPhotoImage', <String, dynamic>{
'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<Map> extractMotionPhotoVideo(AvesEntry entry) async {
try {

View file

@ -56,7 +56,7 @@ class _BugReportState extends State<BugReport> 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,

View file

@ -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),

View file

@ -103,7 +103,7 @@ class _LicensesState extends State<Licenses> {
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),

View file

@ -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),

View file

@ -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<CollectionSource>();
return FilterQuickChooser<String>(
return MenuQuickChooser<String>(
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,
),

View file

@ -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<T extends ChooserQuickButton<U>, U> exten
Curve get animationCurve => Curves.easeOutQuad;
bool get hasChooser => widget.onChooserValue != null;
Widget buildChooser(Animation<double> animation, PopupMenuPosition chooserPosition);
ValueNotifier<U?> get chooserValueNotifier => _chooserValueNotifier;
@ -50,19 +52,18 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, 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<T extends ChooserQuickButton<U>, U> exten
child: IconButton(
icon: icon,
onPressed: widget.onPressed,
tooltip: isChooserEnabled ? null : tooltip,
tooltip: _hasChooser ? null : tooltip,
),
);
}

View file

@ -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<T> extends StatefulWidget {
class MenuQuickChooser<T> extends StatefulWidget {
final ValueNotifier<T?> valueNotifier;
final List<T> options;
final bool autoReverse;
final bool blurred;
final PopupMenuPosition chooserPosition;
final Stream<Offset> 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<T> 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<FilterQuickChooser<T>> createState() => _FilterQuickChooserState<T>();
State<MenuQuickChooser<T>> createState() => _MenuQuickChooserState<T>();
}
class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
final List<StreamSubscription> _subscriptions = [];
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
@ -42,7 +44,7 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
List<T> 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<T> extends State<FilterQuickChooser<T>> {
}
@override
void didUpdateWidget(covariant FilterQuickChooser<T> oldWidget) {
void didUpdateWidget(covariant MenuQuickChooser<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
@ -66,11 +68,11 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
super.dispose();
}
void _registerWidget(FilterQuickChooser<T> widget) {
void _registerWidget(MenuQuickChooser<T> widget) {
_subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove));
}
void _unregisterWidget(FilterQuickChooser<T> widget) {
void _unregisterWidget(MenuQuickChooser<T> widget) {
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
@ -89,7 +91,7 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
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();

View file

@ -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<MoveButton, String> {
@override
Widget buildChooser(Animation<double> 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<CollectionSource>();
final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet();

View file

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

View file

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

View file

@ -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<ShareAction> {
final Set<AvesEntry> entries;
const ShareButton({
super.key,
required super.blurred,
required this.entries,
super.onChooserValue,
required super.onPressed,
});
@override
State<ShareButton> createState() => _ShareButtonState();
}
class _ShareButtonState extends ChooserQuickButtonState<ShareButton, ShareAction> {
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<ShareAction> get options => [
if (widget.entries.any((entry) => entry.isMotionPhoto)) ...[
ShareAction.imageOnly,
ShareAction.videoOnly,
],
];
@override
Widget buildChooser(Animation<double> 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,
),
),
);
}
}

View file

@ -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<ShareAction?> valueNotifier;
final List<ShareAction> options;
final bool autoReverse;
final bool blurred;
final PopupMenuPosition chooserPosition;
final Stream<Offset> 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<ShareAction>(
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(),
),
),
),
);
}
}

View file

@ -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<TagButton, CollectionFilte
@override
Widget buildChooser(Animation<double> 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<CollectionSource>();
final filters = source.sortedTags.map(TagFilter.new).whereNot(options.contains).toSet();

View file

@ -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<CollectionFilter>(
return MenuQuickChooser<CollectionFilter>(
valueNotifier: valueNotifier,
options: options,
autoReverse: true,
blurred: blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,
buildFilterChip: (context, filter) => AvesFilterChip(
itemBuilder: (context, filter) => AvesFilterChip(
filter: filter,
showGenericIcon: false,
),

View file

@ -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),
),
],
);
}

View file

@ -131,9 +131,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
),
),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxHeight,
),
constraints: BoxConstraints(maxHeight: maxHeight),
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: tabs
@ -179,9 +177,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
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,

View file

@ -147,9 +147,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
children: [
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(
minHeight: kMinInteractiveDimension,
),
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Row(
children: [
Icon(icon),

View file

@ -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<void> 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<void> _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);

View file

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

View file

@ -64,9 +64,7 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
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),

View file

@ -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},

View file

@ -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",