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.Handler
import android.os.Looper
import android.os.TransactionTooLargeException
import android.util.Log
import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat
@ -280,7 +281,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val title = call.argument<String>("title")
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
if (urisByMimeType == null) {
result.error("setAs-args", "missing arguments", null)
result.error("share-args", "missing arguments", null)
return
}
@ -288,15 +289,14 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val mimeTypes = urisByMimeType.keys.toTypedArray()
// 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 mimeType = mimeTypes.first()
val intent = Intent(Intent.ACTION_SEND)
Intent(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType)
.putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri))
safeStartActivityChooser(title, intent)
} else {
var mimeType = "*/*"
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)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
.setType(mimeType)
safeStartActivityChooser(title, intent)
}
result.success(started)
try {
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 {

View file

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

View file

@ -148,32 +148,34 @@ class PlatformAndroidAppService implements AndroidAppService {
}
@override
Future<bool> shareEntries(Iterable<AvesEntry> entries) async {
// 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
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
Future<bool> shareEntries(Iterable<AvesEntry> entries) {
return _share(groupBy<AvesEntry, String>(
entries,
// 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 {
final result = await _platform.invokeMethod('share', <String, dynamic>{
'urisByMimeType': urisByMimeType,
});
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
@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);
if (e.code == 'share-large') {
throw TooManyItemsException();
} else {
await reportService.recordError(e, stack);
}
}
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/collection_lens.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/services.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();
}
void _share(BuildContext context) {
Future<void> _share(BuildContext context) async {
final entries = _getTargetItems(context);
androidAppService.shareEntries(entries).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
try {
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) {
@ -447,25 +458,20 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
if (unsupported.isEmpty) return supported;
final unsupportedTypes = unsupported.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort();
final l10n = context.l10n;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) {
final l10n = context.l10n;
return AvesDialog(
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
actions: [
builder: (context) => AvesDialog(
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
actions: [
const CancelButton(),
if (supported.isNotEmpty)
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
onPressed: () => Navigator.pop(context, true),
child: Text(l10n.continueButtonLabel),
),
if (supported.isNotEmpty)
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text(l10n.continueButtonLabel),
),
],
);
},
],
),
);
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 {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) {
return AvesDialog(
content: Text(context.l10n.genericDangerWarningDialogMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.l10n.applyButtonLabel),
),
],
);
},
builder: (context) => AvesDialog(
content: Text(context.l10n.genericDangerWarningDialogMessage),
actions: [
const CancelButton(),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.l10n.applyButtonLabel),
),
],
),
);
if (confirmed == null || !confirmed) return;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -152,19 +152,34 @@ class DialogTitle extends StatelessWidget {
}
}
void showNoMatchingAppDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AvesDialog(
Future<void> showNoMatchingAppDialog(BuildContext context) => showDialog(
context: context,
builder: (context) => AvesDialog(
content: Text(context.l10n.noMatchingAppDialogMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
);
},
);
actions: const [OkButton()],
),
);
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),
],
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
const CancelButton(),
if (needConfirmation)
TextButton(
onPressed: () => Navigator.pop(context, _selectedValue),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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