#434 share quick action to share parts of motion photo
This commit is contained in:
parent
b8510e9676
commit
9208d66e22
29 changed files with 316 additions and 58 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
27
lib/model/actions/share_actions.dart
Normal file
27
lib/model/actions/share_actions.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
|
|
@ -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();
|
|
@ -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';
|
||||
|
|
@ -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 {
|
||||
|
|
60
lib/widgets/common/app_bar/quick_choosers/share_button.dart
Normal file
60
lib/widgets/common/app_bar/quick_choosers/share_button.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
47
lib/widgets/common/app_bar/quick_choosers/share_chooser.dart
Normal file
47
lib/widgets/common/app_bar/quick_choosers/share_chooser.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue