Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active May 9, 2025 09:51
Show Gist options
  • Save PlugFox/c857c181a5a90eec465cadb60df97ee4 to your computer and use it in GitHub Desktop.
Save PlugFox/c857c181a5a90eec465cadb60df97ee4 to your computer and use it in GitHub Desktop.
Flutter file picker
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:meta/meta.dart';
@internal
abstract final class BytesUtil {
/// Extract hash from a [Uint8List] and convert it to a hex string.
static String sha256(Uint8List bytes) {
if (bytes.isEmpty) return 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
crypto.Digest digest = crypto.sha256.convert(bytes);
return digest.toString();
}
}
import 'package:file_picker/file_picker.dart' as file_picker;
/// Pick a file to attach to the next message.
void _pickFile() => runZonedGuarded<void>(
() async {
const allowedExtensions = <String>{
'txt',
'pdf',
'doc',
'docx',
'png',
'jpg',
'jpeg',
'gif',
'bpm',
//'webp',
'heic',
'heics',
'heif',
'heifs',
'hif',
};
const maxFileSize = 20 * 1024 * 1024; // 20 MB
const maxFileCount = 15; // 15 files max can be attached at once
final file_picker.FilePickerResult? result;
try {
final filePicker = file_picker.FilePicker.platform;
result = await filePicker.pickFiles(
dialogTitle: _localization.chatInputTooltipAttachFile,
allowMultiple: false, // allow multiple files to be selected
withData: true, // bytes for web/mobile preview
type: file_picker.FileType.custom,
readSequential: false,
withReadStream: false,
allowedExtensions: allowedExtensions.toList(growable: false),
);
} on Object catch (e, s) {
l.w('File picker not available: $e', s);
if (_disposed) return;
// ignore: use_build_context_synchronously
ErrorUtil.displayErrorSnackBar(context, 'Can\'t pick files', s);
return;
}
if (_disposed) return;
if (_chatController.state case ChatState$Processing(blockInput: true)) return;
if (result == null || result.count < 1 || result.files.isEmpty) return;
final attachedAt = DateTime.now().toUtc();
final stopwatch = Stopwatch()..start();
final newFiles =
await Stream<file_picker.PlatformFile>.fromIterable(result.files)
.take(maxFileCount)
.asyncMap<AttachmentFile>((e) async {
if (stopwatch.elapsed > Duration(milliseconds: 8)) {
// Releave the event loop
await Future<void>.delayed(Duration.zero);
stopwatch.reset();
}
final name = path.basename(e.name);
var extension = e.extension?.toLowerCase() ?? path.extension(name).toLowerCase();
if (extension.startsWith('.')) extension = extension.substring(1);
if (extension.isEmpty) extension = 'bin';
final type = AttachmentFile.extensionToType(extension);
var bytes = e.bytes ?? Uint8List(0);
if (bytes.length > maxFileSize) bytes = Uint8List(0);
return AttachmentFile(
hash: BytesUtil.sha256(bytes),
name: name,
extension: extension,
type: type,
bytes: bytes,
attachedAt: attachedAt,
);
})
.where(
(f) =>
f.name.isNotEmpty &&
f.extension.isNotEmpty &&
f.bytes.isNotEmpty &&
f.size > 0 &&
f.size <= maxFileSize,
)
.toList();
stopwatch.stop();
if (_disposed || newFiles.isEmpty) return;
// All previous attachments that are not in the new list
// To avoid duplicates
final attachmentsMap = <String, AttachmentFile>{
// Previous attachments
for (final f in _attachments.value) f.hash: f,
// New attachments
for (final f in newFiles) f.hash: f,
};
var attachments = attachmentsMap.values.toList(growable: false)
..sort((a, b) => b.attachedAt.compareTo(a.attachedAt));
if (_disposed) return;
if (attachments.length > maxFileCount) attachments = attachments.take(maxFileCount).toList(growable: false);
_attachments.value = attachments;
},
(e, s) {
l.e('Error while picking file: $e', s);
if (_disposed) return;
// ignore: use_build_context_synchronously
ErrorUtil.displayErrorSnackBar(context, e, s);
},
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment