130 lines
4.2 KiB
Dart
130 lines
4.2 KiB
Dart
import 'package:app_settings/app_settings.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
import '../api/errors/permission_exception.dart';
|
|
import 'confirm_dialog.dart';
|
|
import 'info_dialog.dart';
|
|
|
|
/// Thin wrapper around the platform pickers. Every method resolves to `null`
|
|
/// when the user cancels **or** when picking failed; failures are reported to
|
|
/// the user right here (permission denial with a shortcut to the system
|
|
/// settings, everything else as a plain error dialog) so call sites can treat
|
|
/// the result like a cancel.
|
|
class FilePick {
|
|
static final _picker = ImagePicker();
|
|
|
|
static Future<List<XFile>?> multipleGalleryPick(BuildContext context) =>
|
|
_guarded(context, () async {
|
|
final pickedImages = await _picker.pickMultiImage();
|
|
return pickedImages.isNotEmpty ? pickedImages : null;
|
|
});
|
|
|
|
static Future<XFile?> singleGalleryPick(
|
|
BuildContext context, {
|
|
bool forCrop = false,
|
|
}) => _pickImage(context, ImageSource.gallery, forCrop: forCrop);
|
|
|
|
static Future<XFile?> cameraPick(
|
|
BuildContext context, {
|
|
bool forCrop = false,
|
|
}) => _pickImage(context, ImageSource.camera, forCrop: forCrop);
|
|
|
|
/// [forCrop] makes the picker re-encode to a bounded JPEG: the Dart cropper
|
|
/// can't decode HEIC (iOS gallery default) and is slow on full-size photos.
|
|
static Future<XFile?> _pickImage(
|
|
BuildContext context,
|
|
ImageSource source, {
|
|
required bool forCrop,
|
|
}) => _guarded(
|
|
context,
|
|
() => _picker.pickImage(
|
|
source: source,
|
|
maxWidth: forCrop ? 2048 : null,
|
|
maxHeight: forCrop ? 2048 : null,
|
|
imageQuality: forCrop ? 90 : null,
|
|
),
|
|
);
|
|
|
|
static Future<List<String>?> documentPick() async {
|
|
final result = await FilePicker.pickFiles(allowMultiple: true);
|
|
return result?.files.map((e) => e.path).nonNulls.toList();
|
|
}
|
|
|
|
static Future<T?> _guarded<T>(
|
|
BuildContext context,
|
|
Future<T?> Function() pick,
|
|
) async {
|
|
try {
|
|
return await pick();
|
|
} on PlatformException catch (e) {
|
|
final denied = PermissionException.fromPlatform(e);
|
|
if (!context.mounted) return null;
|
|
if (denied != null) {
|
|
_showPermissionDialog(context, denied);
|
|
} else {
|
|
_showErrorDialog(context, e);
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
if (context.mounted) _showErrorDialog(context, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static void _showPermissionDialog(
|
|
BuildContext context,
|
|
PermissionException denied,
|
|
) => ConfirmDialog(
|
|
icon: Icons.block_outlined,
|
|
title: 'Zugriff verweigert',
|
|
content: denied.userMessage,
|
|
confirmButton: 'Einstellungen öffnen',
|
|
onConfirm: AppSettings.openAppSettings,
|
|
).asDialog(context);
|
|
|
|
static void _showErrorDialog(BuildContext context, Object error) =>
|
|
InfoDialog.showError(context, error);
|
|
}
|
|
|
|
/// "Aus Galerie wählen" / "Foto aufnehmen" tiles for an image-source sheet.
|
|
/// The optional [transform] (e.g. a cropper) runs with the sheet still
|
|
/// mounted; the sheet pops only once [onPicked] received the final bytes.
|
|
List<Widget> imageSourceTiles(
|
|
BuildContext sheetContext, {
|
|
required void Function(Uint8List bytes) onPicked,
|
|
Future<Uint8List?> Function(Uint8List bytes)? transform,
|
|
}) {
|
|
Widget tile(IconData icon, String title, Future<XFile?> Function() pick) =>
|
|
ListTile(
|
|
leading: Icon(icon),
|
|
title: Text(title),
|
|
onTap: () async {
|
|
final picked = await pick();
|
|
if (picked == null) return;
|
|
Uint8List? bytes = await picked.readAsBytes();
|
|
if (transform != null) {
|
|
if (!sheetContext.mounted) return;
|
|
bytes = await transform(bytes);
|
|
}
|
|
if (bytes == null || !sheetContext.mounted) return;
|
|
onPicked(bytes);
|
|
Navigator.of(sheetContext).pop();
|
|
},
|
|
);
|
|
return [
|
|
tile(
|
|
Icons.photo_library_outlined,
|
|
'Aus Galerie wählen',
|
|
() => FilePick.singleGalleryPick(sheetContext, forCrop: true),
|
|
),
|
|
tile(
|
|
Icons.photo_camera_outlined,
|
|
'Foto aufnehmen',
|
|
() => FilePick.cameraPick(sheetContext, forCrop: true),
|
|
),
|
|
];
|
|
}
|