#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: optionally show rating & tags on overlay
- Viewer: long press on copy/move/rating/tag quick action for quicker action - 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 - Search: missing address, portrait, landscape filters
- Map: edit cluster location - Map: edit cluster location
- Lithuanian translation (thanks Gediminas Murauskas) - 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) } "getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
"extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) } "extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) }
"extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) } "extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) }
"extractXmpDataProp" -> ioScope.launch { safe(call, result, ::extractXmpDataProp) } "extractXmpDataProp" -> ioScope.launch { safe(call, result, ::extractXmpDataProp) }
@ -83,6 +84,27 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
result.success(thumbnails) 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) { private fun extractMotionPhotoVideo(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
@ -166,9 +188,9 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
try { try {
val embedBytes: ByteArray = if (props.size == 1) { val embedBytes: ByteArray = if (props.size == 1) {
val prop = props.first() as XMPPropName 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 { } else {
xmpDirs.mapNotNull { it.xmpMeta.getSafeStructField(props) }.first().let { xmpDirs.firstNotNullOf { it.xmpMeta.getSafeStructField(props) }.let {
XMPUtils.decodeBase64(it.value) XMPUtils.decodeBase64(it.value)
} }
} }

View file

@ -90,6 +90,8 @@
"entryActionFlip": "Flip horizontally", "entryActionFlip": "Flip horizontally",
"entryActionPrint": "Print", "entryActionPrint": "Print",
"entryActionShare": "Share", "entryActionShare": "Share",
"entryActionShareImageOnly": "Share image only",
"entryActionShareVideoOnly": "Share video only",
"entryActionViewSource": "View source", "entryActionViewSource": "View source",
"entryActionShowGeoTiffOnMap": "Show as map overlay", "entryActionShowGeoTiffOnMap": "Show as map overlay",
"entryActionConvertMotionPhotoToStillImage": "Convert to still image", "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 { abstract class EmbeddedDataService {
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry); Future<List<Uint8List>> getExifThumbnails(AvesEntry entry);
Future<Map> extractMotionPhotoImage(AvesEntry entry);
Future<Map> extractMotionPhotoVideo(AvesEntry entry); Future<Map> extractMotionPhotoVideo(AvesEntry entry);
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry); Future<Map> extractVideoEmbeddedPicture(AvesEntry entry);
@ -31,6 +33,22 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
return []; 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 @override
Future<Map> extractMotionPhotoVideo(AvesEntry entry) async { Future<Map> extractMotionPhotoVideo(AvesEntry entry) async {
try { try {

View file

@ -56,7 +56,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
children: [ children: [
ExpansionPanel( ExpansionPanel(
headerBuilder: (context, isExpanded) => ConstrainedBox( headerBuilder: (context, isExpanded) => ConstrainedBox(
constraints: const BoxConstraints(minHeight: 48), constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,

View file

@ -15,7 +15,7 @@ class AboutCredits extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(minHeight: 48), constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Align( child: Align(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: Text(l10n.aboutCreditsSectionTitle, style: Constants.knownTitleTextStyle), child: Text(l10n.aboutCreditsSectionTitle, style: Constants.knownTitleTextStyle),

View file

@ -103,7 +103,7 @@ class _LicensesState extends State<Licenses> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(minHeight: 48), constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Align( child: Align(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: Text(context.l10n.aboutLicensesSectionTitle, style: Constants.knownTitleTextStyle), child: Text(context.l10n.aboutLicensesSectionTitle, style: Constants.knownTitleTextStyle),

View file

@ -53,7 +53,7 @@ class AboutTranslators extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(minHeight: 48), constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Align( child: Align(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: Text(l10n.aboutTranslatorsSectionTitle, style: Constants.knownTitleTextStyle), 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/filters/album.dart';
import 'package:aves/model/source/collection_source.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:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -26,13 +26,14 @@ class AlbumQuickChooser extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
return FilterQuickChooser<String>( return MenuQuickChooser<String>(
valueNotifier: valueNotifier, valueNotifier: valueNotifier,
options: options, options: options,
autoReverse: true,
blurred: blurred, blurred: blurred,
chooserPosition: chooserPosition, chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition, pointerGlobalPosition: pointerGlobalPosition,
buildFilterChip: (context, album) => AvesFilterChip( itemBuilder: (context, album) => AvesFilterChip(
filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)),
showGenericIcon: false, showGenericIcon: false,
), ),

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/theme/durations.dart'; 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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -35,6 +35,8 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
Curve get animationCurve => Curves.easeOutQuad; Curve get animationCurve => Curves.easeOutQuad;
bool get hasChooser => widget.onChooserValue != null;
Widget buildChooser(Animation<double> animation, PopupMenuPosition chooserPosition); Widget buildChooser(Animation<double> animation, PopupMenuPosition chooserPosition);
ValueNotifier<U?> get chooserValueNotifier => _chooserValueNotifier; ValueNotifier<U?> get chooserValueNotifier => _chooserValueNotifier;
@ -50,19 +52,18 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final onChooserValue = widget.onChooserValue; final _hasChooser = hasChooser;
final isChooserEnabled = onChooserValue != null;
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onLongPressStart: isChooserEnabled ? _showChooser : null, onLongPressStart: _hasChooser ? _showChooser : null,
onLongPressMoveUpdate: isChooserEnabled ? _moveUpdateStreamController.add : null, onLongPressMoveUpdate: _hasChooser ? _moveUpdateStreamController.add : null,
onLongPressEnd: isChooserEnabled onLongPressEnd: _hasChooser
? (details) { ? (details) {
_clearChooserOverlayEntry(); _clearChooserOverlayEntry();
final selectedValue = _chooserValueNotifier.value; final selectedValue = _chooserValueNotifier.value;
if (selectedValue != null) { if (selectedValue != null) {
onChooserValue(selectedValue); widget.onChooserValue?.call(selectedValue);
} }
} }
: null, : null,
@ -70,7 +71,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
child: IconButton( child: IconButton(
icon: icon, icon: icon,
onPressed: widget.onPressed, onPressed: widget.onPressed,
tooltip: isChooserEnabled ? null : tooltip, tooltip: _hasChooser ? null : tooltip,
), ),
); );
} }

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/theme/durations.dart'; 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:aves_ui/aves_ui.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.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:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class FilterQuickChooser<T> extends StatefulWidget { class MenuQuickChooser<T> extends StatefulWidget {
final ValueNotifier<T?> valueNotifier; final ValueNotifier<T?> valueNotifier;
final List<T> options; final List<T> options;
final bool autoReverse;
final bool blurred; final bool blurred;
final PopupMenuPosition chooserPosition; final PopupMenuPosition chooserPosition;
final Stream<Offset> pointerGlobalPosition; 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; static const int maxOptionCount = 5;
FilterQuickChooser({ MenuQuickChooser({
super.key, super.key,
required this.valueNotifier, required this.valueNotifier,
required List<T> options, required List<T> options,
required this.autoReverse,
required this.blurred, required this.blurred,
required this.chooserPosition, required this.chooserPosition,
required this.pointerGlobalPosition, required this.pointerGlobalPosition,
required this.buildFilterChip, required this.itemBuilder,
}) : options = options.take(maxOptionCount).toList(); }) : options = options.take(maxOptionCount).toList();
@override @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 List<StreamSubscription> _subscriptions = [];
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero); final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
@ -42,7 +44,7 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
List<T> get options => widget.options; 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; static const double intraPadding = 8;
@ -54,7 +56,7 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
} }
@override @override
void didUpdateWidget(covariant FilterQuickChooser<T> oldWidget) { void didUpdateWidget(covariant MenuQuickChooser<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget); _unregisterWidget(oldWidget);
_registerWidget(widget); _registerWidget(widget);
@ -66,11 +68,11 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
super.dispose(); super.dispose();
} }
void _registerWidget(FilterQuickChooser<T> widget) { void _registerWidget(MenuQuickChooser<T> widget) {
_subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove)); _subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove));
} }
void _unregisterWidget(FilterQuickChooser<T> widget) { void _unregisterWidget(MenuQuickChooser<T> widget) {
_subscriptions _subscriptions
..forEach((sub) => sub.cancel()) ..forEach((sub) => sub.cancel())
..clear(); ..clear();
@ -89,7 +91,7 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
final isFirst = index == (reversed ? options.length - 1 : 0); final isFirst = index == (reversed ? options.length - 1 : 0);
return Padding( return Padding(
padding: EdgeInsets.only(top: isFirst ? intraPadding : 0, bottom: intraPadding), padding: EdgeInsets.only(top: isFirst ? intraPadding : 0, bottom: intraPadding),
child: widget.buildFilterChip(context, value), child: widget.itemBuilder(context, value),
); );
}).toList(); }).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/settings/settings.dart';
import 'package:aves/model/source/collection_source.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/album_chooser.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/filter_chooser.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/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -39,7 +39,7 @@ class _MoveButtonState extends ChooserQuickButtonState<MoveButton, String> {
@override @override
Widget buildChooser(Animation<double> animation, PopupMenuPosition chooserPosition) { Widget buildChooser(Animation<double> animation, PopupMenuPosition chooserPosition) {
final options = settings.recentDestinationAlbums; final options = settings.recentDestinationAlbums;
final takeCount = FilterQuickChooser.maxOptionCount - options.length; final takeCount = MenuQuickChooser.maxOptionCount - options.length;
if (takeCount > 0) { if (takeCount > 0) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet(); 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/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:aves/widgets/common/app_bar/quick_choosers/rate_chooser.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/theme/icons.dart'; 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'; import 'package:flutter/material.dart';
class RateQuickChooser extends StatefulWidget { 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/filters/tag.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.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/common/button.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/app_bar/quick_choosers/tag_chooser.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/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
@ -36,7 +36,7 @@ class _TagButtonState extends ChooserQuickButtonState<TagButton, CollectionFilte
@override @override
Widget buildChooser(Animation<double> animation, PopupMenuPosition chooserPosition) { Widget buildChooser(Animation<double> animation, PopupMenuPosition chooserPosition) {
final options = settings.recentTags; final options = settings.recentTags;
final takeCount = FilterQuickChooser.maxOptionCount - options.length; final takeCount = MenuQuickChooser.maxOptionCount - options.length;
if (takeCount > 0) { if (takeCount > 0) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
final filters = source.sortedTags.map(TagFilter.new).whereNot(options.contains).toSet(); final filters = source.sortedTags.map(TagFilter.new).whereNot(options.contains).toSet();

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/filters/filters.dart'; 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:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -23,13 +23,14 @@ class TagQuickChooser extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FilterQuickChooser<CollectionFilter>( return MenuQuickChooser<CollectionFilter>(
valueNotifier: valueNotifier, valueNotifier: valueNotifier,
options: options, options: options,
autoReverse: true,
blurred: blurred, blurred: blurred,
chooserPosition: chooserPosition, chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition, pointerGlobalPosition: pointerGlobalPosition,
buildFilterChip: (context, filter) => AvesFilterChip( itemBuilder: (context, filter) => AvesFilterChip(
filter: filter, filter: filter,
showGenericIcon: false, showGenericIcon: false,
), ),

View file

@ -15,6 +15,7 @@ class MenuRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
if (icon != null) if (icon != null)
Padding( Padding(
@ -26,7 +27,9 @@ class MenuRow extends StatelessWidget {
child: icon!, child: icon!,
), ),
), ),
Expanded(child: Text(text)), Flexible(
child: Text(text),
),
], ],
); );
} }

View file

@ -131,9 +131,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
), ),
), ),
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(maxHeight: maxHeight),
maxHeight: maxHeight,
),
child: TabBarView( child: TabBarView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: tabs children: tabs
@ -179,9 +177,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
final availableBodyWidth = constraints.maxWidth; final availableBodyWidth = constraints.maxWidth;
final maxWidth = min(availableBodyWidth, tabBodyMaxWidth(context)); final maxWidth = min(availableBodyWidth, tabBodyMaxWidth(context));
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(maxWidth: maxWidth),
maxWidth: maxWidth,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,

View file

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

View file

@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/move_type.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/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_metadata_edition.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) { void quickRate(BuildContext context, int rating) {
final targetEntry = _getTargetEntry(context, EntryAction.editRating); final targetEntry = _getTargetEntry(context, EntryAction.editRating);
_metadataActionDelegate.quickRate(context, targetEntry, rating); _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/theme/format.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/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/quick_choosers/rate_button.dart';
import 'package:aves/widgets/common/app_bar/tag_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/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.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(); if (_playingOnDragStart) controller!.play();
}, },
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
minHeight: kMinInteractiveDimension,
),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), 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/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/app_bar/favourite_toggler.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/quick_choosers/move_button.dart';
import 'package:aves/widgets/common/app_bar/rate_button.dart'; import 'package:aves/widgets/common/app_bar/quick_choosers/rate_button.dart';
import 'package:aves/widgets/common/app_bar/tag_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/menu.dart';
import 'package:aves/widgets/common/basic/popup_menu_button.dart'; import 'package:aves/widgets/common/basic/popup_menu_button.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
@ -223,6 +224,14 @@ class ViewerButtonRowContent extends StatelessWidget {
onPressed: onPressed, onPressed: onPressed,
); );
break; break;
case EntryAction.share:
child = ShareButton(
blurred: blurred,
entries: {mainEntry},
onChooserValue: (action) => _entryActionDelegate.quickShare(context, action),
onPressed: onPressed,
);
break;
case EntryAction.toggleFavourite: case EntryAction.toggleFavourite:
child = FavouriteToggler( child = FavouriteToggler(
entries: {favouriteTargetEntry}, entries: {favouriteTargetEntry},

View file

@ -56,6 +56,8 @@
"entryActionFlip", "entryActionFlip",
"entryActionPrint", "entryActionPrint",
"entryActionShare", "entryActionShare",
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryActionViewSource", "entryActionViewSource",
"entryActionShowGeoTiffOnMap", "entryActionShowGeoTiffOnMap",
"entryActionConvertMotionPhotoToStillImage", "entryActionConvertMotionPhotoToStillImage",
@ -595,10 +597,14 @@
], ],
"de": [ "de": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation"
], ],
"el": [ "el": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
@ -607,6 +613,8 @@
], ],
"es": [ "es": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation"
], ],
@ -667,6 +675,8 @@
"entryActionFlip", "entryActionFlip",
"entryActionPrint", "entryActionPrint",
"entryActionShare", "entryActionShare",
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryActionViewSource", "entryActionViewSource",
"entryActionShowGeoTiffOnMap", "entryActionShowGeoTiffOnMap",
"entryActionConvertMotionPhotoToStillImage", "entryActionConvertMotionPhotoToStillImage",
@ -1206,10 +1216,14 @@
], ],
"fr": [ "fr": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation"
], ],
"gl": [ "gl": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionExportMetadata", "entryInfoActionExportMetadata",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
@ -1672,6 +1686,8 @@
], ],
"id": [ "id": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionExportMetadata", "entryInfoActionExportMetadata",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
@ -1688,6 +1704,8 @@
], ],
"it": [ "it": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
@ -1697,6 +1715,8 @@
"ja": [ "ja": [
"chipActionFilterIn", "chipActionFilterIn",
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionExportMetadata", "entryInfoActionExportMetadata",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
@ -1713,18 +1733,26 @@
], ],
"ko": [ "ko": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation"
], ],
"lt": [ "lt": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation"
], ],
"nb": [ "nb": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation"
], ],
"nl": [ "nl": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionExportMetadata", "entryInfoActionExportMetadata",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
@ -1743,6 +1771,8 @@
"nn": [ "nn": [
"sourceStateLoading", "sourceStateLoading",
"sourceStateCataloguing", "sourceStateCataloguing",
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterBinLabel", "filterBinLabel",
"filterNoLocationLabel", "filterNoLocationLabel",
@ -2191,6 +2221,8 @@
"timeMinutes", "timeMinutes",
"timeDays", "timeDays",
"focalLength", "focalLength",
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionExportMetadata", "entryInfoActionExportMetadata",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
@ -2688,6 +2720,8 @@
], ],
"pt": [ "pt": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionExportMetadata", "entryInfoActionExportMetadata",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
@ -2704,6 +2738,8 @@
], ],
"ro": [ "ro": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
@ -2712,6 +2748,8 @@
], ],
"ru": [ "ru": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation"
], ],
@ -2722,6 +2760,8 @@
"timeDays", "timeDays",
"focalLength", "focalLength",
"applyButtonLabel", "applyButtonLabel",
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryActionShowGeoTiffOnMap", "entryActionShowGeoTiffOnMap",
"videoActionCaptureFrame", "videoActionCaptureFrame",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
@ -3091,10 +3131,14 @@
], ],
"tr": [ "tr": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation"
], ],
"zh": [ "zh": [
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation", "entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",