handle TransactionTooLargeException when sharing

This commit is contained in:
Thibault Deckers 2023-01-14 23:47:23 +01:00
parent c276ac904b
commit d8a1d21f6c
36 changed files with 272 additions and 307 deletions

View file

@ -10,6 +10,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.TransactionTooLargeException
import android.util.Log import android.util.Log
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
@ -280,7 +281,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val title = call.argument<String>("title") val title = call.argument<String>("title")
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType") val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
if (urisByMimeType == null) { if (urisByMimeType == null) {
result.error("setAs-args", "missing arguments", null) result.error("share-args", "missing arguments", null)
return return
} }
@ -288,15 +289,14 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val mimeTypes = urisByMimeType.keys.toTypedArray() val mimeTypes = urisByMimeType.keys.toTypedArray()
// simplify share intent for a single item, as some apps can handle one item but not more // simplify share intent for a single item, as some apps can handle one item but not more
val started = if (uriList.size == 1) { val intent = if (uriList.size == 1) {
val uri = uriList.first() val uri = uriList.first()
val mimeType = mimeTypes.first() val mimeType = mimeTypes.first()
val intent = Intent(Intent.ACTION_SEND) Intent(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType) .setType(mimeType)
.putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri)) .putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri))
safeStartActivityChooser(title, intent)
} else { } else {
var mimeType = "*/*" var mimeType = "*/*"
if (mimeTypes.size == 1) { if (mimeTypes.size == 1) {
@ -311,14 +311,21 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
} }
val intent = Intent(Intent.ACTION_SEND_MULTIPLE) Intent(Intent.ACTION_SEND_MULTIPLE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList) .putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
.setType(mimeType) .setType(mimeType)
safeStartActivityChooser(title, intent)
} }
try {
result.success(started) val started = safeStartActivityChooser(title, intent)
result.success(started)
} catch (e: Exception) {
if (e is TransactionTooLargeException || e.cause is TransactionTooLargeException) {
result.error("share-large", "transaction too large with ${uriList.size} URIs", e)
} else {
result.error("share-exception", "failed to share ${uriList.size} URIs", e)
}
}
} }
private fun safeStartActivity(intent: Intent): Boolean { private fun safeStartActivity(intent: Intent): Boolean {

View file

@ -438,6 +438,8 @@
"genericFailureFeedback": "Failed", "genericFailureFeedback": "Failed",
"genericDangerWarningDialogMessage": "Are you sure?", "genericDangerWarningDialogMessage": "Are you sure?",
"tooManyItemsErrorDialogMessage": "Try again with fewer items.",
"menuActionConfigureView": "View", "menuActionConfigureView": "View",
"menuActionSelect": "Select", "menuActionSelect": "Select",
"menuActionSelectAll": "Select all", "menuActionSelectAll": "Select all",

View file

@ -148,32 +148,34 @@ class PlatformAndroidAppService implements AndroidAppService {
} }
@override @override
Future<bool> shareEntries(Iterable<AvesEntry> entries) async { Future<bool> shareEntries(Iterable<AvesEntry> entries) {
// loosen MIME type to a generic one, so we can share with badly defined apps return _share(groupBy<AvesEntry, String>(
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats entries,
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); // loosen MIME type to a generic one, so we can share with badly defined apps
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats
(e) => e.mimeTypeAnySubtype,
).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())));
}
@override
Future<bool> shareSingle(String uri, String mimeType) {
return _share({
mimeType: [uri]
});
}
Future<bool> _share(Map<String, List<String>> urisByMimeType) async {
try { try {
final result = await _platform.invokeMethod('share', <String, dynamic>{ final result = await _platform.invokeMethod('share', <String, dynamic>{
'urisByMimeType': urisByMimeType, 'urisByMimeType': urisByMimeType,
}); });
if (result != null) return result as bool; if (result != null) return result as bool;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); if (e.code == 'share-large') {
} throw TooManyItemsException();
return false; } else {
} await reportService.recordError(e, stack);
}
@override
Future<bool> shareSingle(String uri, String mimeType) async {
try {
final result = await _platform.invokeMethod('share', <String, dynamic>{
'urisByMimeType': {
mimeType: [uri]
},
});
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
} }
return false; return false;
} }
@ -207,3 +209,5 @@ class PlatformAndroidAppService implements AndroidAppService {
} }
} }
} }
class TooManyItemsException implements Exception {}

View file

@ -17,6 +17,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
@ -252,11 +253,21 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
return groupedEntries.expand((entry) => entry.burstEntries ?? {entry}).toSet(); return groupedEntries.expand((entry) => entry.burstEntries ?? {entry}).toSet();
} }
void _share(BuildContext context) { Future<void> _share(BuildContext context) async {
final entries = _getTargetItems(context); final entries = _getTargetItems(context);
androidAppService.shareEntries(entries).then((success) { try {
if (!success) showNoMatchingAppDialog(context); if (!await androidAppService.shareEntries(entries)) {
}); await showNoMatchingAppDialog(context);
}
} on TooManyItemsException catch (_) {
await showDialog(
context: context,
builder: (context) => AvesDialog(
content: Text(context.l10n.tooManyItemsErrorDialogMessage),
actions: const [OkButton()],
),
);
}
} }
void _rescan(BuildContext context) { void _rescan(BuildContext context) {
@ -447,25 +458,20 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
if (unsupported.isEmpty) return supported; if (unsupported.isEmpty) return supported;
final unsupportedTypes = unsupported.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort(); final unsupportedTypes = unsupported.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort();
final l10n = context.l10n;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
final l10n = context.l10n; content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
return AvesDialog( actions: [
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))), const CancelButton(),
actions: [ if (supported.isNotEmpty)
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context, true),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel), child: Text(l10n.continueButtonLabel),
), ),
if (supported.isNotEmpty) ],
TextButton( ),
onPressed: () => Navigator.pop(context, true),
child: Text(l10n.continueButtonLabel),
),
],
);
},
); );
if (confirmed == null || !confirmed) return null; if (confirmed == null || !confirmed) return null;
@ -536,21 +542,16 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
Future<void> removeLocation(BuildContext context, Set<AvesEntry> entries) async { Future<void> removeLocation(BuildContext context, Set<AvesEntry> entries) async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
return AvesDialog( content: Text(context.l10n.genericDangerWarningDialogMessage),
content: Text(context.l10n.genericDangerWarningDialogMessage), actions: [
actions: [ const CancelButton(),
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context, true),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel), child: Text(context.l10n.applyButtonLabel),
), ),
TextButton( ],
onPressed: () => Navigator.pop(context, true), ),
child: Text(context.l10n.applyButtonLabel),
),
],
);
},
); );
if (confirmed == null || !confirmed) return; if (confirmed == null || !confirmed) return;

View file

@ -123,21 +123,16 @@ mixin EntryEditorMixin {
if (entries.any((entry) => entry.isMotionPhoto) && types.contains(MetadataType.xmp)) { if (entries.any((entry) => entry.isMotionPhoto) && types.contains(MetadataType.xmp)) {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
return AvesDialog( content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage),
content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage), actions: [
actions: [ const CancelButton(),
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context, true),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel), child: Text(context.l10n.applyButtonLabel),
), ),
TextButton( ],
onPressed: () => Navigator.pop(context, true), ),
child: Text(context.l10n.applyButtonLabel),
),
],
);
},
); );
if (confirmed == null || !confirmed) return null; if (confirmed == null || !confirmed) return null;
} }

View file

@ -136,21 +136,20 @@ mixin FeedbackMixin {
int? itemCount, int? itemCount,
VoidCallback? onCancel, VoidCallback? onCancel,
void Function(Set<T> processed)? onDone, void Function(Set<T> processed)? onDone,
}) { }) =>
return showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => ReportOverlay<T>( builder: (context) => ReportOverlay<T>(
opStream: opStream, opStream: opStream,
itemCount: itemCount, itemCount: itemCount,
onCancel: onCancel, onCancel: onCancel,
onDone: (processed) { onDone: (processed) {
Navigator.pop(context); Navigator.pop(context);
onDone?.call(processed); onDone?.call(processed);
}, },
), ),
); );
}
} }
class ReportOverlay<T> extends StatefulWidget { class ReportOverlay<T> extends StatefulWidget {

View file

@ -54,10 +54,7 @@ mixin PermissionAwareMixin {
return AvesDialog( return AvesDialog(
content: Text(l10n.storageAccessDialogMessage(directory, volume)), content: Text(l10n.storageAccessDialogMessage(directory, volume)),
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
child: Text(MaterialLocalizations.of(context).okButtonLabel), child: Text(MaterialLocalizations.of(context).okButtonLabel),
@ -72,17 +69,10 @@ mixin PermissionAwareMixin {
if (!await deviceService.isSystemFilePickerEnabled()) { if (!await deviceService.isSystemFilePickerEnabled()) {
await showDialog( await showDialog(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
return AvesDialog( content: Text(context.l10n.missingSystemFilePickerDialogMessage),
content: Text(context.l10n.missingSystemFilePickerDialogMessage), actions: const [OkButton()],
actions: [ ),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
);
},
); );
return false; return false;
} }
@ -103,12 +93,7 @@ mixin PermissionAwareMixin {
final volume = dir.getVolumeDescription(context); final volume = dir.getVolumeDescription(context);
return AvesDialog( return AvesDialog(
content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)), content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)),
actions: [ actions: const [OkButton()],
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
); );
}, },
); );

View file

@ -85,12 +85,7 @@ mixin SizeAwareMixin {
final volume = destinationVolume.getDescription(context); final volume = destinationVolume.getDescription(context);
return AvesDialog( return AvesDialog(
content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)), content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)),
actions: [ actions: const [OkButton()],
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
); );
}, },
); );

View file

@ -94,10 +94,7 @@ class _ColorPickerDialogState extends State<ColorPickerDialog> {
) )
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, color), onPressed: () => Navigator.pop(context, color),
child: Text(context.l10n.applyButtonLabel), child: Text(context.l10n.applyButtonLabel),

View file

@ -87,10 +87,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
), ),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {

View file

@ -110,10 +110,7 @@ class _AvesConfirmationDialogState extends State<_AvesConfirmationDialog> {
), ),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: () { onPressed: () {
if (_skip.value) { if (_skip.value) {

View file

@ -152,19 +152,34 @@ class DialogTitle extends StatelessWidget {
} }
} }
void showNoMatchingAppDialog(BuildContext context) { Future<void> showNoMatchingAppDialog(BuildContext context) => showDialog(
showDialog( context: context,
context: context, builder: (context) => AvesDialog(
builder: (context) {
return AvesDialog(
content: Text(context.l10n.noMatchingAppDialogMessage), content: Text(context.l10n.noMatchingAppDialogMessage),
actions: [ actions: const [OkButton()],
TextButton( ),
onPressed: () => Navigator.pop(context), );
child: Text(MaterialLocalizations.of(context).okButtonLabel),
), class CancelButton extends StatelessWidget {
], const CancelButton({super.key});
);
}, @override
); Widget build(BuildContext context) {
return TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
);
}
}
class OkButton extends StatelessWidget {
const OkButton({super.key});
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
);
}
} }

View file

@ -86,10 +86,7 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
if (verticalPadding != 0) SizedBox(height: verticalPadding), if (verticalPadding != 0) SizedBox(height: verticalPadding),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
if (needConfirmation) if (needConfirmation)
TextButton( TextButton(
onPressed: () => Navigator.pop(context, _selectedValue), onPressed: () => Navigator.pop(context, _selectedValue),

View file

@ -88,10 +88,7 @@ class _DurationDialogState extends State<DurationDialog> {
), ),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
AnimatedBuilder( AnimatedBuilder(
animation: Listenable.merge([_minutes, _seconds]), animation: Listenable.merge([_minutes, _seconds]),
builder: (context, child) { builder: (context, child) {

View file

@ -112,10 +112,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
), ),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: () => _submit(context), onPressed: () => _submit(context),
child: Text(l10n.applyButtonLabel), child: Text(l10n.applyButtonLabel),

View file

@ -44,10 +44,7 @@ class _EditEntryTitleDescriptionDialogState extends State<EditEntryTitleDescript
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: fields.isEmpty ? null : () => _submit(context), onPressed: fields.isEmpty ? null : () => _submit(context),
child: Text(context.l10n.applyButtonLabel), child: Text(context.l10n.applyButtonLabel),

View file

@ -126,10 +126,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {

View file

@ -99,10 +99,7 @@ class _EditEntryRatingDialogState extends State<EditEntryRatingDialog> {
), ),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: isValid ? () => _submit(context) : null, onPressed: isValid ? () => _submit(context) : null,
child: Text(l10n.applyButtonLabel), child: Text(l10n.applyButtonLabel),

View file

@ -80,10 +80,7 @@ class _RemoveEntryMetadataDialogState extends State<RemoveEntryMetadataDialog> {
), ),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {

View file

@ -58,10 +58,7 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
onSubmitted: (_) => _submit(context), onSubmitted: (_) => _submit(context),
), ),
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {

View file

@ -114,10 +114,7 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {

View file

@ -155,10 +155,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
spacing: AvesDialog.buttonPadding.horizontal / 2, spacing: AvesDialog.buttonPadding.horizontal / 2,
overflowAlignment: OverflowBarAlignment.end, overflowAlignment: OverflowBarAlignment.end,
children: [ children: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: () { onPressed: () {
final entry = _isCustomEntry ? _customEntry : null; final entry = _isCustomEntry ? _customEntry : null;

View file

@ -90,10 +90,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
), ),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {

View file

@ -57,10 +57,7 @@ class _RenameAlbumDialogState extends State<RenameAlbumDialog> {
); );
}), }),
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {

View file

@ -145,10 +145,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
), ),
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
key: const Key('button-apply'), key: const Key('button-apply'),
onPressed: () { onPressed: () {

View file

@ -57,10 +57,7 @@ class _VideoSpeedDialogState extends State<VideoSpeedDialog> {
], ],
), ),
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: () => _submit(context), onPressed: () => _submit(context),
child: Text(context.l10n.applyButtonLabel), child: Text(context.l10n.applyButtonLabel),

View file

@ -80,10 +80,7 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
] ]
: null, : null,
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
if (canSelect) if (canSelect)
TextButton( TextButton(
onPressed: () => _submit(context), onPressed: () => _submit(context),

View file

@ -39,10 +39,7 @@ class _WallpaperSettingsDialogState extends State<WallpaperSettingsDialog> {
) )
], ],
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, Tuple2<WallpaperTarget, bool>(_selectedTarget, _useScrollEffect)), onPressed: () => Navigator.pop(context, Tuple2<WallpaperTarget, bool>(_selectedTarget, _useScrollEffect)),
child: Text(context.l10n.applyButtonLabel), child: Text(context.l10n.applyButtonLabel),

View file

@ -234,21 +234,16 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
return AvesDialog( content: Text(filters.length == 1 ? l10n.deleteSingleAlbumConfirmationDialogMessage(todoCount) : l10n.deleteMultiAlbumConfirmationDialogMessage(todoCount)),
content: Text(filters.length == 1 ? l10n.deleteSingleAlbumConfirmationDialogMessage(todoCount) : l10n.deleteMultiAlbumConfirmationDialogMessage(todoCount)), actions: [
actions: [ const CancelButton(),
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context, true),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel), child: Text(l10n.deleteButtonLabel),
), ),
TextButton( ],
onPressed: () => Navigator.pop(context, true), ),
child: Text(l10n.deleteButtonLabel),
),
],
);
},
); );
if (confirmed == null || !confirmed) return; if (confirmed == null || !confirmed) return;

View file

@ -53,21 +53,16 @@ class ChipActionDelegate {
Future<void> _hide(BuildContext context, CollectionFilter filter) async { Future<void> _hide(BuildContext context, CollectionFilter filter) async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
return AvesDialog( content: Text(context.l10n.hideFilterConfirmationDialogMessage),
content: Text(context.l10n.hideFilterConfirmationDialogMessage), actions: [
actions: [ const CancelButton(),
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context, true),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel), child: Text(context.l10n.hideButtonLabel),
), ),
TextButton( ],
onPressed: () => Navigator.pop(context, true), ),
child: Text(context.l10n.hideButtonLabel),
),
],
);
},
); );
if (confirmed == null || !confirmed) return; if (confirmed == null || !confirmed) return;

View file

@ -304,21 +304,16 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
Future<void> _hide(BuildContext context, Set<T> filters) async { Future<void> _hide(BuildContext context, Set<T> filters) async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
return AvesDialog( content: Text(context.l10n.hideFilterConfirmationDialogMessage),
content: Text(context.l10n.hideFilterConfirmationDialogMessage), actions: [
actions: [ const CancelButton(),
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context, true),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel), child: Text(context.l10n.hideButtonLabel),
), ),
TextButton( ],
onPressed: () => Navigator.pop(context, true), ),
child: Text(context.l10n.hideButtonLabel),
),
],
);
},
); );
if (confirmed == null || !confirmed) return; if (confirmed == null || !confirmed) return;

View file

@ -54,10 +54,7 @@ class _AppExportItemSelectionDialogState extends State<AppExportItemSelectionDia
); );
}).toList(), }).toList(),
actions: [ actions: [
TextButton( const CancelButton(),
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton( TextButton(
onPressed: _selectedItems.isEmpty ? null : () => Navigator.pop(context, _selectedItems), onPressed: _selectedItems.isEmpty ? null : () => Navigator.pop(context, _selectedItems),
child: Text(context.l10n.applyButtonLabel), child: Text(context.l10n.applyButtonLabel),

View file

@ -114,27 +114,22 @@ class SettingsTileDisplayForceTvLayout extends SettingsTile {
selector: (context, s) => s.forceTvLayout, selector: (context, s) => s.forceTvLayout,
onChanged: (v) async { onChanged: (v) async {
if (v) { if (v) {
final l10n = context.l10n;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
final l10n = context.l10n; content: Text([
return AvesDialog( l10n.settingsModificationWarningDialogMessage,
content: Text([ l10n.genericDangerWarningDialogMessage,
l10n.settingsModificationWarningDialogMessage, ].join('\n\n')),
l10n.genericDangerWarningDialogMessage, actions: [
].join('\n\n')), const CancelButton(),
actions: [ TextButton(
TextButton( onPressed: () => Navigator.pop(context, true),
onPressed: () => Navigator.pop(context), child: Text(l10n.applyButtonLabel),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ),
), ],
TextButton( ),
onPressed: () => Navigator.pop(context, true),
child: Text(l10n.applyButtonLabel),
),
],
);
},
); );
if (confirmed == null || !confirmed) return; if (confirmed == null || !confirmed) return;
} }

View file

@ -229,21 +229,16 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
Future<void> _convertMotionPhotoToStillImage(BuildContext context, AvesEntry targetEntry) async { Future<void> _convertMotionPhotoToStillImage(BuildContext context, AvesEntry targetEntry) async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
return AvesDialog( content: Text(context.l10n.genericDangerWarningDialogMessage),
content: Text(context.l10n.genericDangerWarningDialogMessage), actions: [
actions: [ const CancelButton(),
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context, true),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel), child: Text(context.l10n.applyButtonLabel),
), ),
TextButton( ],
onPressed: () => Navigator.pop(context, true), ),
child: Text(context.l10n.applyButtonLabel),
),
],
);
},
); );
if (confirmed == null || !confirmed) return; if (confirmed == null || !confirmed) return;

View file

@ -66,21 +66,19 @@ abstract class AvesVideoController {
final resume = await showDialog<bool>( final resume = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) => AvesDialog(
return AvesDialog( content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))),
content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))), actions: [
actions: [ TextButton(
TextButton( onPressed: () => Navigator.pop(context),
onPressed: () => Navigator.pop(context), child: Text(context.l10n.videoStartOverButtonLabel),
child: Text(context.l10n.videoStartOverButtonLabel), ),
), TextButton(
TextButton( onPressed: () => Navigator.pop(context, true),
onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.videoResumeButtonLabel),
child: Text(context.l10n.videoResumeButtonLabel), ),
), ],
], ),
);
},
); );
if (resume == null || !resume) return 0; if (resume == null || !resume) return 0;
return resumeTime; return resumeTime;

View file

@ -226,6 +226,7 @@
"genericSuccessFeedback", "genericSuccessFeedback",
"genericFailureFeedback", "genericFailureFeedback",
"genericDangerWarningDialogMessage", "genericDangerWarningDialogMessage",
"tooManyItemsErrorDialogMessage",
"menuActionConfigureView", "menuActionConfigureView",
"menuActionSelect", "menuActionSelect",
"menuActionSelectAll", "menuActionSelectAll",
@ -572,6 +573,7 @@
"cs": [ "cs": [
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
@ -581,6 +583,7 @@
"columnCount", "columnCount",
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
@ -590,11 +593,16 @@
"el": [ "el": [
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
], ],
"es": [
"tooManyItemsErrorDialogMessage"
],
"fa": [ "fa": [
"clearTooltip", "clearTooltip",
"videoActionPause", "videoActionPause",
@ -708,6 +716,7 @@
"genericSuccessFeedback", "genericSuccessFeedback",
"genericFailureFeedback", "genericFailureFeedback",
"genericDangerWarningDialogMessage", "genericDangerWarningDialogMessage",
"tooManyItemsErrorDialogMessage",
"menuActionConfigureView", "menuActionConfigureView",
"menuActionSelect", "menuActionSelect",
"menuActionSelectAll", "menuActionSelectAll",
@ -1035,6 +1044,10 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"fr": [
"tooManyItemsErrorDialogMessage"
],
"gl": [ "gl": [
"columnCount", "columnCount",
"entryActionShareImageOnly", "entryActionShareImageOnly",
@ -1151,6 +1164,7 @@
"genericSuccessFeedback", "genericSuccessFeedback",
"genericFailureFeedback", "genericFailureFeedback",
"genericDangerWarningDialogMessage", "genericDangerWarningDialogMessage",
"tooManyItemsErrorDialogMessage",
"menuActionConfigureView", "menuActionConfigureView",
"menuActionSelect", "menuActionSelect",
"menuActionSelectAll", "menuActionSelectAll",
@ -1507,9 +1521,14 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"id": [
"tooManyItemsErrorDialogMessage"
],
"it": [ "it": [
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
@ -1526,6 +1545,7 @@
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"subtitlePositionTop", "subtitlePositionTop",
"subtitlePositionBottom", "subtitlePositionBottom",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
@ -1533,11 +1553,16 @@
"settingsWidgetDisplayedItem" "settingsWidgetDisplayedItem"
], ],
"ko": [
"tooManyItemsErrorDialogMessage"
],
"lt": [ "lt": [
"columnCount", "columnCount",
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
@ -1552,6 +1577,7 @@
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
@ -1574,6 +1600,7 @@
"subtitlePositionBottom", "subtitlePositionBottom",
"widgetDisplayedItemRandom", "widgetDisplayedItemRandom",
"widgetDisplayedItemMostRecent", "widgetDisplayedItemMostRecent",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowRatingTags", "settingsViewerShowRatingTags",
"settingsViewerShowDescription", "settingsViewerShowDescription",
@ -1603,6 +1630,7 @@
"editEntryLocationDialogSetCustom", "editEntryLocationDialogSetCustom",
"locationPickerUseThisLocationButton", "locationPickerUseThisLocationButton",
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage", "removeEntryMetadataMotionPhotoXmpWarningDialogMessage",
"tooManyItemsErrorDialogMessage",
"viewDialogSortSectionTitle", "viewDialogSortSectionTitle",
"viewDialogReverseSortOrder", "viewDialogReverseSortOrder",
"aboutLinkPolicy", "aboutLinkPolicy",
@ -1870,20 +1898,30 @@
"wallpaperUseScrollEffect" "wallpaperUseScrollEffect"
], ],
"pl": [
"tooManyItemsErrorDialogMessage"
],
"pt": [ "pt": [
"columnCount", "columnCount",
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"widgetDisplayedItemMostRecent", "widgetDisplayedItemMostRecent",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowRatingTags", "settingsViewerShowRatingTags",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
], ],
"ro": [
"tooManyItemsErrorDialogMessage"
],
"ru": [ "ru": [
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
], ],
@ -1922,6 +1960,7 @@
"editEntryDateDialogExtractFromTitle", "editEntryDateDialogExtractFromTitle",
"editEntryDateDialogShift", "editEntryDateDialogShift",
"removeEntryMetadataDialogTitle", "removeEntryMetadataDialogTitle",
"tooManyItemsErrorDialogMessage",
"collectionActionShowTitleSearch", "collectionActionShowTitleSearch",
"collectionActionHideTitleSearch", "collectionActionHideTitleSearch",
"collectionActionAddShortcut", "collectionActionAddShortcut",
@ -2235,9 +2274,18 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"tr": [
"tooManyItemsErrorDialogMessage"
],
"uk": [
"tooManyItemsErrorDialogMessage"
],
"zh": [ "zh": [
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
@ -2248,6 +2296,7 @@
"columnCount", "columnCount",
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",