diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 678b5143..f02f4615 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -435,53 +435,51 @@ class _HomePageState extends State { ); } } else { - // for desktop - final path = await FilePicker.platform.saveFile( + // for desktop: file_picker v13 writes the file itself. + final Uint8List bytes; + if (fileType == ExportFileType.pdf) { + final pdf = await PdfHTMLEncoder( + fontFallback: [ + await PdfGoogleFonts.notoColorEmoji(), + await PdfGoogleFonts.notoColorEmojiRegular(), + ], + ).convert(result); + bytes = await pdf.save(); + } else { + bytes = utf8.encode(result); + } + final uri = await FilePicker.saveFile( fileName: 'document.${fileType.extension}', + bytes: bytes, ); - if (path != null) { - await File(path).writeAsString(result); - if (fileType == ExportFileType.pdf) { - final pdf = await PdfHTMLEncoder( - fontFallback: [ - await PdfGoogleFonts.notoColorEmoji(), - await PdfGoogleFonts.notoColorEmojiRegular(), - ], - ).convert(result); - - await File(path).writeAsBytes(await pdf.save()); - } - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('This document is saved to the $path'), - ), - ); - } + if (uri != null && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('This document is saved to the ${uri.toFilePath()}'), + ), + ); } } } void _importFile(ExportFileType fileType) async { - final result = await FilePicker.platform.pickFiles( - allowMultiple: false, + final file = await FilePicker.pickFile( allowedExtensions: [fileType.extension], type: FileType.custom, ); + if (file == null) { + return; + } var plainText = ''; if (!kIsWeb) { - final path = result?.files.single.path; + final path = file.path; if (path == null) { return; } plainText = await File(path).readAsString(); } else { - final bytes = result?.files.first.bytes; - if (bytes == null) { - return; - } - plainText = const Utf8Decoder().convert(bytes); + // file_picker v13 reads a file's content on demand. + plainText = const Utf8Decoder().convert(await file.readAsBytes()); } var jsonString = ''; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 92da2365..bf3021ed 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: google_fonts: ^6.1.0 flutter_localizations: sdk: flutter - file_picker: ^10.3.10 + file_picker: ^13.1.0 universal_html: ^2.0.8 highlight: ^0.7.0 http: ^1.1.0 diff --git a/lib/src/editor/block_component/image_block_component/image_upload_widget.dart b/lib/src/editor/block_component/image_block_component/image_upload_widget.dart index 7c5a592a..881e332d 100644 --- a/lib/src/editor/block_component/image_block_component/image_upload_widget.dart +++ b/lib/src/editor/block_component/image_block_component/image_upload_widget.dart @@ -335,15 +335,19 @@ class _UploadImageMenuState extends State { dialogTitle: '', type: kIsWeb ? fp.FileType.custom : fp.FileType.image, allowedExtensions: kIsWeb ? allowedExtensions : null, - withData: kIsWeb, ); if (result != null && result.files.isNotEmpty) { + final file = result.files.first; + // file_picker v13 reads a file's content on demand. + final bytes = kIsWeb ? await file.readAsBytes() : null; + if (!mounted) { + return; + } setState(() { - final bytes = result.files.first.bytes; - if (kIsWeb && bytes != null) { + if (bytes != null) { _imagePathOrContent = base64String(bytes); } else { - _imagePathOrContent = result.files.first.path; + _imagePathOrContent = file.path; } }); } diff --git a/lib/src/editor/util/file_picker/file_picker_impl.dart b/lib/src/editor/util/file_picker/file_picker_impl.dart index e36d3020..cb55f42d 100644 --- a/lib/src/editor/util/file_picker/file_picker_impl.dart +++ b/lib/src/editor/util/file_picker/file_picker_impl.dart @@ -1,12 +1,19 @@ +import 'dart:typed_data'; + import 'package:appflowy_editor/src/editor/util/file_picker/file_picker_service.dart'; import 'package:file_picker/file_picker.dart' as fp; +/// [FilePickerService] on top of file_picker v13, whose API is static and +/// returns the picked files directly. class FilePicker implements FilePickerService { @override Future getDirectoryPath({String? title}) { - return fp.FilePicker.platform.getDirectoryPath(); + return fp.FilePicker.getDirectoryPath(dialogTitle: title); } + /// [withData], [withReadStream] and [lockParentWindow] no longer exist in + /// file_picker v13 and are ignored: read a picked file's content with + /// `PlatformFile.readAsBytes()` or `PlatformFile.readAsByteStream()`. @override Future pickFiles({ String? dialogTitle, @@ -19,21 +26,31 @@ class FilePicker implements FilePickerService { bool withReadStream = false, bool lockParentWindow = false, }) async { - final result = await fp.FilePicker.platform.pickFiles( + if (allowMultiple) { + final files = await fp.FilePicker.pickFiles( + dialogTitle: dialogTitle, + initialDirectory: initialDirectory, + type: type, + allowedExtensions: allowedExtensions, + onFileLoading: onFileLoading, + ); + return FilePickerResult(files); + } + + final file = await fp.FilePicker.pickFile( dialogTitle: dialogTitle, initialDirectory: initialDirectory, type: type, allowedExtensions: allowedExtensions, onFileLoading: onFileLoading, - allowMultiple: allowMultiple, - withData: withData, - withReadStream: withReadStream, - lockParentWindow: lockParentWindow, ); - - return FilePickerResult(result?.files ?? []); + return FilePickerResult([?file]); } + /// file_picker v13 writes the file itself and needs its content up front, + /// while this service only asks where to save. So it saves an empty file + /// there and returns its path (or its URI where there is no local path) for + /// the caller to write to. [lockParentWindow] is ignored. @override Future saveFile({ String? dialogTitle, @@ -42,14 +59,18 @@ class FilePicker implements FilePickerService { fp.FileType type = fp.FileType.any, List? allowedExtensions, bool lockParentWindow = false, - }) { - return fp.FilePicker.platform.saveFile( + }) async { + final uri = await fp.FilePicker.saveFile( + fileName: fileName ?? 'untitled', + bytes: Uint8List(0), dialogTitle: dialogTitle, - fileName: fileName, initialDirectory: initialDirectory, type: type, allowedExtensions: allowedExtensions, - lockParentWindow: lockParentWindow, ); + if (uri == null) { + return null; + } + return uri.scheme == 'file' ? uri.toFilePath() : uri.toString(); } } diff --git a/pubspec.yaml b/pubspec.yaml index fddb79ca..931109e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,17 +5,17 @@ version: 6.1.0 homepage: "https://github.com/AppFlowy-IO/appflowy-editor" environment: - sdk: ">=3.6.0 <4.0.0" - flutter: ">=3.32.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.6" description: A highly customizable rich-text editor for Flutter. The AppFlowy Editor project for AppFlowy and beyond. dependencies: collection: "^1.18.0" - device_info_plus: ^12.3.0 + device_info_plus: ^13.2.0 diff_match_patch: "^0.4.1" - file_picker: ^10.3.10 + file_picker: ^13.1.0 flutter: sdk: flutter flutter_svg: "^2.0.6"