import 'dart:async'; import 'package:universal_html/html.dart' as html; class PickedCsvFile { final String name; final String content; const PickedCsvFile({required this.name, required this.content}); } abstract class CsvInputService { Future pickCsv(); } class WebCsvInputService implements CsvInputService { @override Future pickCsv() async { final input = html.FileUploadInputElement() ..accept = '.csv,text/csv' ..multiple = false ..style.display = 'none'; html.document.body?.append(input); final file = await _pickFile(input); if (file == null) return null; final reader = html.FileReader(); final readCompleter = Completer(); reader.onError.listen((_) { readCompleter.completeError(StateError('Failed to read file')); }); reader.onLoadEnd.listen((_) { final result = reader.result; if (result is String) { readCompleter.complete(result); } else { readCompleter.completeError(StateError('Unsupported file payload')); } }); reader.readAsText(file); final content = await readCompleter.future; return PickedCsvFile(name: file.name, content: content); } Future _pickFile(html.FileUploadInputElement input) async { final completer = Completer(); void completeWith(html.File? file) { if (!completer.isCompleted) completer.complete(file); } StreamSubscription? changeSub; StreamSubscription? inputSub; StreamSubscription? focusSub; Timer? timeout; void cleanup() { changeSub?.cancel(); inputSub?.cancel(); focusSub?.cancel(); timeout?.cancel(); input.remove(); } void tryComplete() { final file = input.files?.isNotEmpty == true ? input.files!.first : null; if (file != null) { completeWith(file); } } changeSub = input.onChange.listen((_) => tryComplete()); inputSub = input.onInput.listen((_) => tryComplete()); focusSub = html.window.onFocus.listen((_) async { if (completer.isCompleted) return; for (var i = 0; i < 6; i++) { tryComplete(); if (completer.isCompleted) return; await Future.delayed(const Duration(milliseconds: 120)); } if (!completer.isCompleted && (input.value ?? '').isEmpty) { completeWith(null); } }); timeout = Timer(const Duration(minutes: 2), () => completeWith(null)); completer.future.whenComplete(cleanup); input.click(); return completer.future; } }