Files
Client/lib/widget/file_pick.dart
T

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),
),
];
}