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?> multipleGalleryPick(BuildContext context) => _guarded(context, () async { final pickedImages = await _picker.pickMultiImage(); return pickedImages.isNotEmpty ? pickedImages : null; }); static Future singleGalleryPick( BuildContext context, { bool forCrop = false, }) => _pickImage(context, ImageSource.gallery, forCrop: forCrop); static Future 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 _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?> documentPick() async { final result = await FilePicker.pickFiles(allowMultiple: true); return result?.files.map((e) => e.path).nonNulls.toList(); } static Future _guarded( BuildContext context, Future 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 imageSourceTiles( BuildContext sheetContext, { required void Function(Uint8List bytes) onPicked, Future Function(Uint8List bytes)? transform, }) { Widget tile(IconData icon, String title, Future 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), ), ]; }