main
dart 251 lines 7.92 KB
Raw
1 // ignore_for_file: deprecated_member_use
2 import 'dart:io';
3
4 import 'package:file_picker/file_picker.dart';
5 import 'package:flutter/material.dart';
6 import 'package:flutter/services.dart';
7 import 'package:image_picker/image_picker.dart';
8
9 import '../../l10n/app_localizations.dart';
10 import '../logger/app_logger.dart';
11
12 /// Safe wrapper around [FilePicker] and [ImagePicker] platform operations.
13 ///
14 /// Prevents unhandled [PlatformException]s (e.g. `explorer_not_found` on Android)
15 /// from crashing the app when no default file manager or gallery is available.
16 class SafeFilePicker {
17 SafeFilePicker._();
18
19 /// Determines whether an error is caused by a missing/disabled file manager activity.
20 static bool isExplorerNotFoundError(Object error) {
21 if (error is PlatformException) {
22 if (error.code == 'explorer_not_found') return true;
23 final msg = error.message?.toLowerCase() ?? '';
24 if (msg.contains('explorer') ||
25 msg.contains('activity to handle the request') ||
26 msg.contains('no activity found to handle intent') ||
27 msg.contains('file explorer installed')) {
28 return true;
29 }
30 }
31 final str = error.toString().toLowerCase();
32 return str.contains('explorer_not_found') ||
33 str.contains('can\'t find a valid activity') ||
34 str.contains('file explorer installed');
35 }
36
37 /// Extracts a localized, user-friendly error message from [error].
38 static String getErrorMessage(
39 Object error,
40 AppLocalizations l10n, {
41 String? fallbackMessage,
42 }) {
43 if (isExplorerNotFoundError(error)) {
44 return l10n.file_explorer_not_found;
45 }
46 if (error is PlatformException) {
47 if (error.message != null && error.message!.trim().isNotEmpty) {
48 return error.message!.trim();
49 }
50 }
51 if (error is FileSystemException) {
52 return error.message.isNotEmpty ? error.message : error.toString();
53 }
54 final message = error
55 .toString()
56 .replaceFirst(RegExp(r'^([A-Za-z0-9_]+Exception|Exception):\s*'), '')
57 .trim();
58 if (message.isNotEmpty) {
59 return message;
60 }
61 return fallbackMessage ?? error.toString();
62 }
63
64 /// Safely picks files using [FilePicker.platform.pickFiles].
65 ///
66 /// Catches [PlatformException] and other errors, logs them, and optionally
67 /// displays a user-friendly [SnackBar] if [context] is provided.
68 static Future<FilePickerResult?> pickFiles({
69 FileType type = FileType.any,
70 List<String>? allowedExtensions,
71 bool allowMultiple = false,
72 bool withData = false,
73 bool withReadStream = false,
74 String? dialogTitle,
75 String? initialDirectory,
76 bool lockParentWindow = false,
77 Function(FilePickerStatus)? onFileLoading,
78 BuildContext? context,
79 void Function(String message)? onError,
80 }) async {
81 try {
82 return await FilePicker.pickFiles(
83 type: type,
84 allowedExtensions: allowedExtensions,
85 allowMultiple: allowMultiple,
86 withData: withData,
87 withReadStream: withReadStream,
88 dialogTitle: dialogTitle,
89 initialDirectory: initialDirectory,
90 lockParentWindow: lockParentWindow,
91 onFileLoading: onFileLoading,
92 );
93 } catch (e, st) {
94 Log.warning('SafeFilePicker.pickFiles failed: $e\n$st');
95 if (context != null && context.mounted) {
96 final l10n = AppLocalizations.of(context);
97 if (l10n != null) {
98 final msg = getErrorMessage(e, l10n);
99 ScaffoldMessenger.maybeOf(
100 context,
101 )?.showSnackBar(SnackBar(content: Text(msg)));
102 }
103 } else if (onError != null) {
104 onError(e.toString());
105 }
106 return null;
107 }
108 }
109
110 /// Safely opens a save file dialog using [FilePicker.saveFile].
111 static Future<String?> saveFile({
112 String? dialogTitle,
113 required String fileName,
114 String? initialDirectory,
115 FileType type = FileType.any,
116 List<String>? allowedExtensions,
117 Uint8List? bytes,
118 bool lockParentWindow = false,
119 BuildContext? context,
120 void Function(String message)? onError,
121 }) async {
122 try {
123 return await FilePicker.saveFile(
124 dialogTitle: dialogTitle,
125 fileName: fileName,
126 initialDirectory: initialDirectory,
127 type: type,
128 allowedExtensions: allowedExtensions,
129 bytes: bytes ?? Uint8List(0),
130 lockParentWindow: lockParentWindow,
131 );
132 } catch (e, st) {
133 Log.warning('SafeFilePicker.saveFile failed: $e\n$st');
134 if (context != null && context.mounted) {
135 final l10n = AppLocalizations.of(context);
136 if (l10n != null) {
137 final msg = getErrorMessage(e, l10n);
138 ScaffoldMessenger.maybeOf(
139 context,
140 )?.showSnackBar(SnackBar(content: Text(msg)));
141 }
142 } else if (onError != null) {
143 onError(e.toString());
144 }
145 return null;
146 }
147 }
148
149 /// Safely gets a directory path using [FilePicker.getDirectoryPath].
150 static Future<String?> getDirectoryPath({
151 String? dialogTitle,
152 String? initialDirectory,
153 bool lockParentWindow = false,
154 BuildContext? context,
155 void Function(String message)? onError,
156 }) async {
157 try {
158 return await FilePicker.getDirectoryPath(
159 dialogTitle: dialogTitle,
160 initialDirectory: initialDirectory,
161 lockParentWindow: lockParentWindow,
162 );
163 } catch (e, st) {
164 Log.warning('SafeFilePicker.getDirectoryPath failed: $e\n$st');
165 if (context != null && context.mounted) {
166 final l10n = AppLocalizations.of(context);
167 if (l10n != null) {
168 final msg = getErrorMessage(e, l10n);
169 ScaffoldMessenger.maybeOf(
170 context,
171 )?.showSnackBar(SnackBar(content: Text(msg)));
172 }
173 } else if (onError != null) {
174 onError(e.toString());
175 }
176 return null;
177 }
178 }
179
180 /// Safely picks multiple images using [ImagePicker.pickMultiImage].
181 static Future<List<XFile>> pickMultiImage({
182 int? imageQuality,
183 double? maxWidth,
184 double? maxHeight,
185 bool requestFullMetadata = true,
186 BuildContext? context,
187 void Function(String message)? onError,
188 }) async {
189 try {
190 final picker = ImagePicker();
191 return await picker.pickMultiImage(
192 imageQuality: imageQuality,
193 maxWidth: maxWidth,
194 maxHeight: maxHeight,
195 requestFullMetadata: requestFullMetadata,
196 );
197 } catch (e, st) {
198 Log.warning('SafeFilePicker.pickMultiImage failed: $e\n$st');
199 if (context != null && context.mounted) {
200 final l10n = AppLocalizations.of(context);
201 if (l10n != null) {
202 final msg = getErrorMessage(e, l10n);
203 ScaffoldMessenger.maybeOf(
204 context,
205 )?.showSnackBar(SnackBar(content: Text(msg)));
206 }
207 } else if (onError != null) {
208 onError(e.toString());
209 }
210 return const [];
211 }
212 }
213
214 /// Safely picks a single image using [ImagePicker.pickImage].
215 static Future<XFile?> pickImage({
216 required ImageSource source,
217 int? imageQuality,
218 double? maxWidth,
219 double? maxHeight,
220 CameraDevice preferredCameraDevice = CameraDevice.rear,
221 bool requestFullMetadata = true,
222 BuildContext? context,
223 void Function(String message)? onError,
224 }) async {
225 try {
226 final picker = ImagePicker();
227 return await picker.pickImage(
228 source: source,
229 imageQuality: imageQuality,
230 maxWidth: maxWidth,
231 maxHeight: maxHeight,
232 preferredCameraDevice: preferredCameraDevice,
233 requestFullMetadata: requestFullMetadata,
234 );
235 } catch (e, st) {
236 Log.warning('SafeFilePicker.pickImage failed: $e\n$st');
237 if (context != null && context.mounted) {
238 final l10n = AppLocalizations.of(context);
239 if (l10n != null) {
240 final msg = getErrorMessage(e, l10n);
241 ScaffoldMessenger.maybeOf(
242 context,
243 )?.showSnackBar(SnackBar(content: Text(msg)));
244 }
245 } else if (onError != null) {
246 onError(e.toString());
247 }
248 return null;
249 }
250 }
251 }