diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de45ce382..9fa6dccae 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,9 +47,13 @@ jobs: - name: 📩 Check if package is ready for publishing run: flutter pub publish --dry-run - - name: đŸ§Ș Run Flutter tests + - name: đŸ§Ș Run flutter_quill tests run: flutter test + - name: đŸ§Ș Run flutter_quill_extensions tests + run: flutter test + working-directory: flutter_quill_extensions + - name: 🔍 Check the translations run: dart ./scripts/translations_check.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 396f4c7a4..5f9784c5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- New localization strings for the image save functionality [#2403](https://github.com/singerdmx/flutter-quill/pull/2403). + ### Changed +- Rewrite the image save functionality for [`flutter_quill_extensions`](https://pub.dev/packages/flutter_quill_extensions) [#2403](https://github.com/singerdmx/flutter-quill/pull/2403). +- Migrate [quill_native_bridge](https://pub.dev/packages/quill_native_bridge) to `11.0.0` [#2403](https://github.com/singerdmx/flutter-quill/pull/2403). - Avoid using deprecated APIs in Flutter 3.27.0: - Migrate from `withOpacity` to `withValues` according to [Color wide gamut - Opacity migration](https://docs.flutter.dev/release/breaking-changes/wide-gamut-framework#opacity). - Avoid using the deprecated `Color.value` getter. - Ignore `unreachable_switch_default` warning (introduced in Dart 3.6). - Update `intl` dependency to support versions `0.19.0` and `0.20.0`. +### Fixed + +- Avoid using [`url_launcher_string.dart`](https://pub.dev/documentation/url_launcher/latest/url_launcher_string/url_launcher_string-library.html) which is [**strongly discouraged**](https://pub.dev/packages/url_launcher#urls-not-handled-by-uri) [#2403](https://github.com/singerdmx/flutter-quill/pull/2403). + ## [11.0.0-dev.14] - 2024-11-24 ### Changed diff --git a/example/.gitignore b/example/.gitignore index 12b4e0432..4e689af84 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index c72122824..b12854181 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,12 @@ + + + + + + + + + + + + + + diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 7d5b8c182..3bcdb912f 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,14 +6,12 @@ import FlutterMacOS import Foundation import file_selector_macos -import gal import quill_native_bridge_macos import url_launcher_macos import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index b3544367a..ce458c1e1 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -1,48 +1,16 @@ PODS: - - file_selector_macos (0.0.1): - - FlutterMacOS - FlutterMacOS (1.0.0) - - gal (1.0.0): - - Flutter - - FlutterMacOS - - quill_native_bridge_macos (0.0.1): - - FlutterMacOS - - url_launcher_macos (0.0.1): - - FlutterMacOS - - video_player_avfoundation (0.0.1): - - Flutter - - FlutterMacOS DEPENDENCIES: - - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - - quill_native_bridge_macos (from `Flutter/ephemeral/.symlinks/plugins/quill_native_bridge_macos/macos`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) EXTERNAL SOURCES: - file_selector_macos: - :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos FlutterMacOS: :path: Flutter/ephemeral - gal: - :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin - quill_native_bridge_macos: - :path: Flutter/ephemeral/.symlinks/plugins/quill_native_bridge_macos/macos - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - video_player_avfoundation: - :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin SPEC CHECKSUMS: - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 - quill_native_bridge_macos: f90985c5269ac7ba84d933605b463d96e5f544fe - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 6d907a32c..4d1aed5d2 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 8E3774B08A93A8D54E597870 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C6627E85DE71088607AABDE /* Pods_RunnerTests.framework */; }; CBFCAD814543F1F46CF7EB02 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B7FF1B5F74BB7EE9AEAADC6 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -103,6 +104,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, CBFCAD814543F1F46CF7EB02 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -240,7 +242,6 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 3C5A23DA7F8245D72F435A4A /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -248,6 +249,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* flutter_quill_example.app */; productType = "com.apple.product-type.application"; @@ -292,6 +296,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -361,23 +368,6 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 3C5A23DA7F8245D72F435A4A /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; ADA7845785713A6AD96ACC83 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -796,6 +786,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e42bdff0..8222cc2bd 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift index 8e02df288..b3c176141 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/example/macos/Runner/AppDelegate.swift @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index ef070abf0..681e73fe2 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -10,7 +10,7 @@ com.apple.security.network.client - com.apple.security.files.user-selected.read-only + com.apple.security.files.user-selected.read-write diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements index 389e8f35f..43a8b8c70 100644 --- a/example/macos/Runner/Release.entitlements +++ b/example/macos/Runner/Release.entitlements @@ -6,7 +6,7 @@ com.apple.security.network.client - com.apple.security.files.user-selected.read-only + com.apple.security.files.user-selected.read-write diff --git a/example/pubspec.lock b/example/pubspec.lock index ff2944691..2d6c3d4cf 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -250,22 +250,6 @@ packages: description: flutter source: sdk version: "0.0.0" - gal: - dependency: transitive - description: - name: gal - sha256: "54c9b72528efce7c66234f3b6dd01cb0304fd8af8196de15571d7bdddb940977" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - gal_linux: - dependency: transitive - description: - name: gal_linux - sha256: "0040d61843134cc5a93e4597080a86f2ba073217957e28b2a684b4d8b050873c" - url: "https://pub.dev" - source: hosted - version: "0.1.2" html: dependency: transitive description: @@ -462,10 +446,10 @@ packages: dependency: transitive description: name: quill_native_bridge - sha256: "0b3200c57bb4f1f12d6c764648d42482891f20f12024c75fe3479cafc1e132c9" + sha256: bda0f0ad9bc160dcdd4bd2b378a7ae8bdb55c3d4b7301bf739d5e3b065ee5e82 url: "https://pub.dev" source: hosted - version: "10.7.11" + version: "11.0.0" quill_native_bridge_android: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5f97bdeb2..8148153c4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' version: 0.1.0+1 environment: - sdk: ^3.0.0 - flutter: ">=3.10.0" + sdk: ^3.5.0 + flutter: ">=3.24.0" dependencies: flutter: diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 236a2f2b3..043a96f01 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -7,14 +7,11 @@ #include "generated_plugin_registrant.h" #include -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); - GalPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("GalPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 5d19bf7c2..a95e2673b 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows - gal url_launcher_windows ) diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 02eeaa691..fcbde1e4d 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -12,8 +12,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Rewrite the image save functionality with support for all platforms [#2403](https://github.com/singerdmx/flutter-quill/pull/2403). - Ignore `unreachable_switch_default` warning (introduced in Dart 3.6). +### Removed + +- The following packages are no longer dependencies of `flutter_quill_extensions`: + * [http](https://pub.dev/packages/http) + * [cross_file](https://pub.dev/packages/cross_file) + ## [11.0.0-dev.4] - 2024-11-24 > [!IMPORTANT] diff --git a/flutter_quill_extensions/README.md b/flutter_quill_extensions/README.md index e40a5a294..a814f676d 100644 --- a/flutter_quill_extensions/README.md +++ b/flutter_quill_extensions/README.md @@ -44,11 +44,9 @@ dependencies: The package uses the following plugins: -1. [`gal`](https://github.com/natsuk4ze/gal) to save images. - Ensure to follow the [gal setup](https://pub.dev/packages/gal#-get-started) guide as it requires platform-specific setup. -2. [`image_picker`](https://pub.dev/packages/image_picker) for picking images. - See the [image_picker installation](https://pub.dev/packages/image_picker#installation) section. -3. [`video_player`](https://pub.dev/packages/video_player) for playing videos. See the [video_player setup](https://pub.dev/packages/video_player#setup) section. +1. [`quill_native_bridge`](https://pub.dev/packages/quill_native_bridge) to save images: [Setup](https://pub.dev/packages/quill_native_bridge#-setup) +2. [`image_picker`](https://pub.dev/packages/image_picker) for picking images: [Setup](https://pub.dev/packages/image_picker#installation) +3. [`video_player`](https://pub.dev/packages/video_player) for video playback: [Setup](https://pub.dev/packages/video_player#setup) ### Loading Images from the Internet @@ -62,8 +60,7 @@ The package uses the following plugins: 2. To restrict image and video loading to HTTPS only, configure your app accordingly. If you need to support HTTP, you must adjust your app settings for release mode. Consult the [Android Cleartext / Plaintext HTTP](https://developer.android.com/privacy-and-security/risks/cleartext-communications) - guide - for more information. + guide for more information. #### macOS diff --git a/flutter_quill_extensions/lib/src/common/utils/utils.dart b/flutter_quill_extensions/lib/src/common/utils/utils.dart index ab13830d7..13d754086 100644 --- a/flutter_quill_extensions/lib/src/common/utils/utils.dart +++ b/flutter_quill_extensions/lib/src/common/utils/utils.dart @@ -1,10 +1,3 @@ -import 'dart:io' show File; - -import 'package:flutter/foundation.dart' show immutable; -import 'package:gal/gal.dart'; -import 'package:http/http.dart' as http; - -import '../../editor/image/widgets/image.dart'; import 'patterns.dart'; bool isBase64(String str) { @@ -35,71 +28,3 @@ bool isYouTubeUrl(String videoUrl) { return false; } } - -enum SaveImageResultMethod { network, localStorage } - -@immutable -class SaveImageResult { - const SaveImageResult({required this.error, required this.method}); - - final String? error; - final SaveImageResultMethod method; -} - -Future saveImage({ - required String imageUrl, -}) async { - final imageFile = File(imageUrl); - final hasPermission = await Gal.hasAccess(); - if (!hasPermission) { - await Gal.requestAccess(); - } - final imageExistsLocally = await imageFile.exists(); - if (!imageExistsLocally) { - try { - await _saveImageFromNetwork( - Uri.parse(appendFileExtensionToImageUrl(imageUrl)), - ); - return const SaveImageResult( - error: null, - method: SaveImageResultMethod.network, - ); - } catch (e) { - return SaveImageResult( - error: e.toString(), - method: SaveImageResultMethod.network, - ); - } - } else { - try { - await _saveLocalImage(Uri.parse(imageUrl)); - return const SaveImageResult( - error: null, - method: SaveImageResultMethod.localStorage, - ); - } catch (e) { - return SaveImageResult( - error: e.toString(), - method: SaveImageResultMethod.localStorage, - ); - } - } -} - -Future _saveImageFromNetwork(Uri imageUrl) async { - final response = await http.get( - imageUrl, - ); - if (response.statusCode != 200) { - throw Exception('Response to $imageUrl is not successful.'); - } - final imageBytes = response.bodyBytes; - await Gal.putImageBytes(imageBytes, - name: imageUrl.pathSegments.isNotEmpty - ? imageUrl.pathSegments.last - : 'image'); -} - -Future _saveLocalImage(Uri imageUrl) async { - await Gal.putImage(imageUrl.toString()); -} diff --git a/flutter_quill_extensions/lib/src/editor/image/image_load_utils.dart b/flutter_quill_extensions/lib/src/editor/image/image_load_utils.dart new file mode 100644 index 000000000..a30af954e --- /dev/null +++ b/flutter_quill_extensions/lib/src/editor/image/image_load_utils.dart @@ -0,0 +1,36 @@ +import 'dart:async' show Completer; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +class ImageLoader { + static ImageLoader _instance = ImageLoader(); + + static ImageLoader get instance => _instance; + + /// Allows overriding the instance for testing + @visibleForTesting + static set instance(ImageLoader newInstance) => _instance = newInstance; + + // TODO(performance): This will load the image again. In case +// this is a network image, then this will be inefficient. + Future loadImageBytesFromImageProvider({ + required ImageProvider imageProvider, + }) async { + final stream = imageProvider.resolve(ImageConfiguration.empty); + final completer = Completer(); + + ImageStreamListener? listener; + listener = ImageStreamListener((info, _) { + completer.complete(info.image); + stream.removeListener(listener!); + }); + + stream.addListener(listener); + + final image = await completer.future; + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + return byteData?.buffer.asUint8List(); + } +} diff --git a/flutter_quill_extensions/lib/src/editor/image/image_menu.dart b/flutter_quill_extensions/lib/src/editor/image/image_menu.dart index ab7bd9463..814f79fd1 100644 --- a/flutter_quill_extensions/lib/src/editor/image/image_menu.dart +++ b/flutter_quill_extensions/lib/src/editor/image/image_menu.dart @@ -1,17 +1,17 @@ -import 'dart:async' show Completer; -import 'dart:ui' as ui; - import 'package:flutter/cupertino.dart' show showCupertinoModalPopup; -import 'package:flutter/foundation.dart' show kIsWeb, Uint8List; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' show ImageUrl, QuillController, StyleAttribute, getEmbedNode; import 'package:flutter_quill/internal.dart'; +import 'package:path/path.dart' as p; +import 'package:url_launcher/url_launcher.dart'; import '../../common/utils/element_utils/element_utils.dart'; import '../../common/utils/string.dart'; -import '../../common/utils/utils.dart'; import 'config/image_config.dart'; +import 'image_load_utils.dart'; +import 'image_save_utils.dart'; import 'widgets/image.dart' show ImageTapWrapper, getImageStyleString; import 'widgets/image_resizer.dart' show ImageResizer; @@ -23,6 +23,7 @@ class ImageOptionsMenu extends StatelessWidget { required this.imageSize, required this.readOnly, required this.imageProvider, + this.prefersGallerySave = true, super.key, }); @@ -33,6 +34,19 @@ class ImageOptionsMenu extends StatelessWidget { final bool readOnly; final ImageProvider imageProvider; + // TODO(quill_native_bridge): Update this doc comment once saveImageToGallery() + // is supported on Windows too (will be applicable like macOS). See https://pub.dev/packages/quill_native_bridge#-features + /// Determines if the image should be saved to the gallery instead of using the + /// system file save dialog for platforms that support both. + /// + /// Currently, the only platform where this applies is macOS. + /// + /// This is silently ignored on platforms that only support gallery save (Android and iOS) + /// or only image save. + /// + /// For more details, refer to [quill_native_bridge Saving images](https://pub.dev/packages/quill_native_bridge#-saving-images). + final bool prefersGallerySave; + @override Widget build(BuildContext context) { final materialTheme = Theme.of(context); @@ -90,7 +104,9 @@ class ImageOptionsMenu extends StatelessWidget { getImageStyleString(controller), ); - final imageBytes = await _loadImageBytesFromImageProvider(); + final imageBytes = await ImageLoader.instance + .loadImageBytesFromImageProvider( + imageProvider: imageProvider); if (imageBytes != null) { await ClipboardServiceProvider.instance.copyImage(imageBytes); } @@ -126,48 +142,90 @@ class ImageOptionsMenu extends StatelessWidget { await config.onImageRemovedCallback.call(imageSource); }, ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.save), - title: Text(context.loc.save), - onTap: () async { - final messenger = ScaffoldMessenger.of(context); - final localizations = context.loc; - Navigator.of(context).pop(); + ListTile( + leading: const Icon(Icons.save), + title: Text(context.loc.save), + onTap: () async { + final messenger = ScaffoldMessenger.of(context); + final localizations = context.loc; + Navigator.of(context).pop(); - final saveImageResult = await saveImage( + SaveImageResult? result; + try { + result = await ImageSaver.instance.saveImage( imageUrl: imageSource, + imageProvider: imageProvider, + prefersGallerySave: prefersGallerySave, ); - final imageSavedSuccessfully = saveImageResult.error == null; + } on GalleryImageSaveAccessDeniedException { + messenger.showSnackBar(SnackBar( + content: Text( + localizations.saveImagePermissionDenied, + ))); + return; + } - messenger.clearSnackBars(); + if (result == null) { + messenger.showSnackBar(SnackBar( + content: Text( + localizations.errorUnexpectedSavingImage, + ))); + return; + } - if (!imageSavedSuccessfully) { - messenger.showSnackBar(SnackBar( - content: Text( - localizations.errorWhileSavingImage, - ))); - return; - } + if (kIsWeb) { + messenger.showSnackBar(SnackBar( + content: Text(localizations.successImageDownloaded))); + return; + } - var message = switch (saveImageResult.method) { - SaveImageResultMethod.network => - localizations.savedUsingTheNetwork, - SaveImageResultMethod.localStorage => - localizations.savedUsingLocalStorage, - }; + if (result.isGallerySave) { + messenger.showSnackBar(SnackBar( + content: Text(localizations.successImageSavedGallery), + action: SnackBarAction( + label: localizations.openGallery, + onPressed: () => + QuillNativeProvider.instance.openGalleryApp(), + ), + )); + return; + } - if (isDesktopApp) { - message = localizations.theImageHasBeenSavedAt(imageSource); + if (isDesktopApp) { + final imageFilePath = result.imageFilePath; + if (imageFilePath == null) { + // User canceled the system save dialog. + return; } messenger.showSnackBar( SnackBar( - content: Text(message), + content: Text(localizations.successImageSaved), + // On macOS the app only has access to the picked file from the system save + // dialog and not the directory where it was saved. + // Opening the directory of that file requires entitlements on macOS + // See https://pub.dev/packages/url_launcher#macos-file-access-configuration + // Open the saved image file instead of the directory + action: defaultTargetPlatform == TargetPlatform.macOS + ? SnackBarAction( + label: localizations.openFile, + onPressed: () => launchUrl(Uri.file(imageFilePath)), + ) + : SnackBarAction( + label: localizations.openFileLocation, + onPressed: () => launchUrl( + Uri.directory(p.dirname(imageFilePath))), + ), ), ); - }, - ), + + return; + } + + throw StateError( + 'Image save result is not handled on $defaultTargetPlatform'); + }, + ), ListTile( leading: const Icon(Icons.zoom_in), title: Text(context.loc.zoom), @@ -185,23 +243,4 @@ class ImageOptionsMenu extends StatelessWidget { ), ); } - - // TODO: This will load the image again, in case it was network image - // then it will send a GET request each time to load the image. - Future _loadImageBytesFromImageProvider() async { - final stream = imageProvider.resolve(ImageConfiguration.empty); - final completer = Completer(); - - ImageStreamListener? listener; - listener = ImageStreamListener((info, _) { - completer.complete(info.image); - stream.removeListener(listener!); - }); - - stream.addListener(listener); - - final image = await completer.future; - final byteData = await image.toByteData(format: ui.ImageByteFormat.png); - return byteData?.buffer.asUint8List(); - } } diff --git a/flutter_quill_extensions/lib/src/editor/image/image_save_utils.dart b/flutter_quill_extensions/lib/src/editor/image/image_save_utils.dart new file mode 100644 index 000000000..5203c999c --- /dev/null +++ b/flutter_quill_extensions/lib/src/editor/image/image_save_utils.dart @@ -0,0 +1,254 @@ +@internal +library; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_quill/internal.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +import 'image_load_utils.dart'; + +const defaultImageFileExtension = 'png'; + +// The [imageSourcePath] could be file, asset path or HTTP image URL. +String extractImageFileExtensionFromImageSource(String? imageSourcePath) { + if (imageSourcePath == null || imageSourcePath.isEmpty) { + return defaultImageFileExtension; + } + + if (!imageSourcePath.contains('.')) { + return defaultImageFileExtension; + } + + return p.extension(imageSourcePath).replaceFirst('.', ''); +} + +// The [imageSourcePath] could be file, asset path or HTTP image URL. +String? extractImageNameFromImageSource(String? imageSourcePath) { + if (imageSourcePath == null || imageSourcePath.isEmpty) { + return null; + } + final uri = Uri.parse(imageSourcePath); + final pathWithoutQuery = uri.path; + + final imageName = p.basenameWithoutExtension(pathWithoutQuery); + if (imageName.isEmpty) { + return null; + } + return imageName; +} + +class SaveImageResult { + const SaveImageResult({ + required this.imageFilePath, + required this.isGallerySave, + }); + + /// Returns `null` on web platforms, if [isGallerySave] is `true` + /// or in case the user cancels the save operation on desktop platforms. + final String? imageFilePath; + + final bool isGallerySave; + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + if (other is! SaveImageResult) return false; + return other.imageFilePath == imageFilePath && + other.isGallerySave == isGallerySave; + } + + @override + int get hashCode => Object.hash(imageFilePath, isGallerySave); + + @override + String toString() => + 'SaveImageResult(imageFilePath: $imageFilePath, isGallerySave: $isGallerySave)'; +} + +const String defaultImageFileNamePrefix = 'IMG'; + +String getDefaultImageFileName({required bool isGallerySave}) { + if (kIsWeb) { + // The browser handles name conflicts. + return defaultImageFileNamePrefix; + } + if (isGallerySave) { + // The gallery app handles name conflicts. + return defaultImageFileNamePrefix; + } + if (defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.windows) { + // Windows and macOS system native save dialog prompts the user to confirm file overwrite. + return defaultImageFileNamePrefix; + } + final uniqueFileName = + '${defaultImageFileNamePrefix}_${DateTime.now().toIso8601String()}'; + if (defaultTargetPlatform == TargetPlatform.linux) { + // IMPORTANT: On Linux, it depends on the desktop environment + // and name conflicts may not be handled. Always provide a unique image file name. + return uniqueFileName; + } + + return uniqueFileName; +} + +Future shouldSaveToGallery({required bool prefersGallerySave}) async { + final supportsGallerySave = await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.saveImageToGallery); + if (!supportsGallerySave) { + return false; + } + final supportsImageSave = await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.saveImage); + if (!supportsImageSave) { + return true; + } + + return supportsGallerySave && prefersGallerySave; +} + +/// Thrown when the gallery image save operation is denied +/// due to insufficient or denied permissions. +class GalleryImageSaveAccessDeniedException implements Exception { + GalleryImageSaveAccessDeniedException([this.message]); + + final String? message; + + @override + String toString() => + message ?? + 'Permission to save the image to the gallery was denied or insufficient.'; +} + +class ImageSaver { + ImageSaver._(); + + static ImageSaver _instance = ImageSaver._(); + + static ImageSaver get instance => _instance; + + /// Allows overriding the instance for testing + @visibleForTesting + static set instance(ImageSaver newInstance) => _instance = newInstance; + + /// Saves an image to the user's device based on the platform: + /// + /// - **Web**: Downloads the image using the browser's download functionality. + /// - **Desktop**: Prompts the user to choose a location for the image using + /// native save dialog, defaulting to the user's `Pictures` directory. Or + /// saves the image to the gallery in case [prefersGallerySave] is `true` and + // TODO(quill_native_bridge): Update this doc comment once saveImageToGallery() + // is supported on Windows too (will be applicable like macOS). See https://pub.dev/packages/quill_native_bridge#-features + /// the gallery is supported (currently only macOS is applicable). + /// - **Mobile**: Saves the image to the gallery, requesting permission if needed. + /// + /// The [imageUrl] could be file or network image URL and is used to extract + /// image file extension and the image name. + /// + /// The [imageProvider] is used to load the image bytes from using [ImageLoader]. + /// + /// Returns `null` on failure. + /// + /// Throws [GalleryImageSaveAccessDeniedException] in case permission was denied or insuffeicnet. + Future saveImage({ + required String imageUrl, + required ImageProvider imageProvider, + required bool prefersGallerySave, + }) async { + assert(() { + if (imageUrl.isEmpty) { + throw ArgumentError.value(imageUrl, 'imageUrl', 'cannot be empty'); + } + return true; + }()); + + final imageFileExtension = + extractImageFileExtensionFromImageSource(imageUrl); + final imageName = extractImageNameFromImageSource(imageUrl); + + final imageBytes = await ImageLoader.instance + .loadImageBytesFromImageProvider(imageProvider: imageProvider); + if (imageBytes == null || imageBytes.isEmpty) { + return null; + } + + if (kIsWeb) { + await QuillNativeProvider.instance.saveImage( + imageBytes, + options: ImageSaveOptions( + name: imageName ?? getDefaultImageFileName(isGallerySave: false), + fileExtension: imageFileExtension), + ); + return const SaveImageResult( + imageFilePath: null, + isGallerySave: false, + ); + } + + if (await shouldSaveToGallery(prefersGallerySave: prefersGallerySave)) { + try { + await QuillNativeProvider.instance.saveImageToGallery( + imageBytes, + options: GalleryImageSaveOptions( + name: imageName ?? getDefaultImageFileName(isGallerySave: true), + fileExtension: imageFileExtension, + // Specifying the album name requires read-write permission + // on iOS and macOS on all versions. Pass null to request add-only on + // supported versions (previous versions still use read-write). + albumName: null, + ), + ); + + return const SaveImageResult( + imageFilePath: null, + isGallerySave: true, + ); + } on PlatformException catch (e) { + // TODO(save-image): Part of https://github.com/FlutterQuill/quill-native-bridge/issues/2 + + // Permission request is required only on iOS, macOS and Android API 28 and earlier. + if (e.code == 'PERMISSION_DENIED') { + // macOS imposes security restrictions when running the app + // on sources other than Xcode or the macOS terminal, such as Android Studio or VS Code. + // This is not an issue in production. Throwing [GalleryImageSaveAccessDeniedException] will indicate + // that the user denied the permission, even though it will always deny the permission even if granted. + // Make sure we don't handle that error (it has details) during development to avoid confusion. + // For more details, see https://github.com/flutter/flutter/issues/134191#issuecomment-2506248266 + // and https://pub.dev/packages/quill_native_bridge#-saving-images-to-the-gallery + + final possiblePermissionIssueDuringDevelopmentOnMacOS = + kDebugMode && defaultTargetPlatform == TargetPlatform.macOS; + if (possiblePermissionIssueDuringDevelopmentOnMacOS) { + rethrow; + } + + throw GalleryImageSaveAccessDeniedException(e.toString()); + } + rethrow; + } + } + + if (await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.saveImage)) { + assert(!isMobileApp, + 'Mobile platforms support saving images to the gallery only'); + + final result = await QuillNativeProvider.instance.saveImage( + imageBytes, + options: ImageSaveOptions( + name: imageName ?? getDefaultImageFileName(isGallerySave: false), + fileExtension: imageFileExtension, + ), + ); + return SaveImageResult( + imageFilePath: result.filePath, + isGallerySave: false, + ); + } + + throw StateError('Image save is not handled on $defaultTargetPlatform'); + } +} diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 8c21ef947..03724288e 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_quill_extensions -description: Embed extensions for flutter_quill including image, video, formula and etc. +description: Embed extensions for flutter_quill to support loading images and videos version: 11.0.0-dev.4 homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ @@ -32,11 +32,9 @@ dependencies: sdk: flutter # Dart Packages - http: ^1.0.0 - path: ^1.8.0 meta: ^1.7.0 universal_html: ^2.2.4 - cross_file: ^0.3.3+4 + path: ^1.8.0 flutter_quill: ^11.0.0-dev.3 photo_view: ^0.15.0 @@ -44,14 +42,13 @@ dependencies: # Plugins video_player: ^2.8.0 url_launcher: ^6.2.1 - gal: ^2.3.0 - gal_linux: ^0.1.0 image_picker: ^1.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 + mocktail: ^1.0.4 flutter: uses-material-design: true diff --git a/flutter_quill_extensions/pubspec_overrides.yaml b/flutter_quill_extensions/pubspec_overrides.yaml new file mode 100644 index 000000000..557e5b0c3 --- /dev/null +++ b/flutter_quill_extensions/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +dependency_overrides: +# TODO(save-image): Remove this file after publishing flutter_quill and update min version + flutter_quill: + path: ../ \ No newline at end of file diff --git a/flutter_quill_extensions/test/editor/image/image_save_utils_test.dart b/flutter_quill_extensions/test/editor/image/image_save_utils_test.dart new file mode 100644 index 000000000..03d7913a2 --- /dev/null +++ b/flutter_quill_extensions/test/editor/image/image_save_utils_test.dart @@ -0,0 +1,958 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/internal.dart'; +import 'package:flutter_quill_extensions/src/editor/image/image_load_utils.dart'; +import 'package:flutter_quill_extensions/src/editor/image/image_save_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +const testImageExtensions = {'png', 'jpeg', 'jpg', 'gif', 'webp'}; + +void main() { + group('extractImageFileExtensionFromImageSource', () { + test('defaults to using png', () { + expect(defaultImageFileExtension, equals('png')); + }); + + test('returns $defaultImageFileExtension when file name is null or empty', + () { + expect(extractImageFileExtensionFromImageSource(null), + equals(defaultImageFileExtension)); + expect(extractImageFileExtensionFromImageSource(''), + equals(defaultImageFileExtension)); + }); + + test('returns $defaultImageFileExtension when file name does not have dot', + () { + expect(extractImageFileExtensionFromImageSource('imagepng'), + equals(defaultImageFileExtension)); + expect(extractImageFileExtensionFromImageSource('image png'), + equals(defaultImageFileExtension)); + expect(extractImageFileExtensionFromImageSource('image'), + equals(defaultImageFileExtension)); + expect(extractImageFileExtensionFromImageSource('png'), + equals(defaultImageFileExtension)); + }); + + test('returns the file extension correctly', () { + for (final fileExtension in testImageExtensions) { + expect(extractImageFileExtensionFromImageSource('image.$fileExtension'), + equals(fileExtension)); + } + }); + }); + group('extractImageNameFromImageSource', () { + test( + 'returns the file name without the extension when a valid name is given', + () { + expect(extractImageNameFromImageSource('image.jpg'), 'image'); + }); + + test('returns null when the input is null or empty', () { + expect(extractImageNameFromImageSource(null), isNull); + expect(extractImageNameFromImageSource(''), isNull); + }); + + test('returns the image name correctly', () { + for (final fileExtension in testImageExtensions) { + expect( + extractImageNameFromImageSource('image.$fileExtension'), 'image'); + } + }); + + group('HTTP URLs', () { + test('extracts image name from a HTTP URL correctly', () { + const imageName = 'image'; + expect( + extractImageNameFromImageSource( + 'https://example.com/path/to/file/$imageName.png'), + equals(imageName), + ); + }); + + test('extracts image name from a URL with query parameters', () { + const imageName = 'quill_image'; + expect( + extractImageNameFromImageSource( + 'https://example.com/path/to/file/$imageName.png?version=1.2.3'), + equals(imageName), + ); + }); + + test( + 'extracts image name from a URL with query parameters and without extension', + () { + const imageName = 'quill_image'; + expect( + extractImageNameFromImageSource( + 'https://example.com/path/to/file/$imageName?version=1.2.3'), + equals(imageName), + ); + }); + + test('extracts image name from a URL with query parameters', () { + const imageName = '2019-Metrology-Events.jpg'; + expect( + extractImageNameFromImageSource( + 'https://firebasestorage.googleapis.com/v0/b/eventat-4ba96.appspot.com/o/$imageName.jpg?alt=media&token=bfc47032-5173-4b3f-86bb-9659f46b362a'), + equals(imageName), + ); + }); + + test('handles a HTTP URL with a trailing slash', () { + expect( + extractImageNameFromImageSource('https://example.com/path/to/file/'), + equals('file'), + ); + }); + + test('handles a URL ending with a slash and no file name', () { + expect( + extractImageNameFromImageSource('https://example.com/path/to/'), + equals('to'), + ); + }); + + test('handles a URL with multiple slashes in the path', () { + const imageName = 'ExampleImage'; + expect( + extractImageNameFromImageSource( + 'https://example.com/path/to/extra/level/$imageName.webp'), + equals(imageName), + ); + }); + + test('returns null for URLs without any path components', () { + expect( + extractImageNameFromImageSource('https://example.com'), + isNull, + ); + }); + + test('extracts image name from a URL with special characters', () { + const imageName = '2013-report-final_v2'; + expect( + extractImageNameFromImageSource( + 'https://example.com/files/$imageName'), + equals(imageName), + ); + }); + }); + + group('File paths', () { + test('extracts image name from a standard file path', () { + const imageName = 'ExampleImage'; + expect( + extractImageNameFromImageSource('/path/to/file/$imageName.gif'), + equals(imageName), + ); + }); + + test('returns null for a path that ends with a trailing slash', () { + const imageName = 'ExampleImage2'; + + expect( + extractImageNameFromImageSource('/path/to/file/$imageName.webp/'), + imageName, + ); + }); + + test('returns null for an empty path', () { + expect( + extractImageNameFromImageSource(''), + isNull, + ); + }); + + test('handles paths without a file name', () { + const imageName = 'emptyfolder'; + expect( + extractImageNameFromImageSource('/path/to/$imageName/'), + equals(imageName), + ); + }); + + test('extracts file name from a file path with special characters', () { + const imageName = '2015-report-final_v2'; + expect( + extractImageNameFromImageSource('/path/to/file/$imageName.png'), + equals(imageName), + ); + }); + + test( + 'returns null for a path that is just a file name with no directories', + () { + const imageName = 'document'; + + expect( + extractImageNameFromImageSource('$imageName.png'), + equals(imageName), + ); + }); + test('handles paths that ends with a slash', () { + const imageName = 'Image'; + expect( + extractImageNameFromImageSource('/path/to/file/$imageName.png/'), + equals(imageName), + ); + }); + }); + }); + + group('$SaveImageResult', () { + test('overrides toString() correctly', () { + const imageFilePath = '/path/to/file'; + const isGallerySave = false; + expect( + const SaveImageResult( + imageFilePath: imageFilePath, isGallerySave: isGallerySave) + .toString(), + 'SaveImageResult(imageFilePath: $imageFilePath, isGallerySave: $isGallerySave)', + ); + }); + + test('implements equality correctly', () { + const imageFilePath = '/path/to/file.gif'; + const isGallerySave = true; + + expect( + const SaveImageResult( + imageFilePath: imageFilePath, isGallerySave: isGallerySave), + const SaveImageResult( + imageFilePath: imageFilePath, isGallerySave: isGallerySave), + reason: 'two instances with the same values should be equal', + ); + }); + + test('overrides hashCode correctly', () { + const imageFilePath = '/path/to/file.webp'; + const isGallerySave = false; + expect( + const SaveImageResult( + imageFilePath: imageFilePath, isGallerySave: isGallerySave) + .hashCode, + const SaveImageResult( + imageFilePath: imageFilePath, isGallerySave: isGallerySave) + .hashCode, + ); + }); + }); + + test('defaultImageFileNamePrefix constant is correct', () { + expect(defaultImageFileNamePrefix, equals('IMG')); + }); + + group('getDefaultImageFileName', () { + if (kIsWeb) { + test('returns default file name prefix when saving on the web', () { + // The browser handles name conflicts. + for (final isGallerySave in {true, false}) { + expect(getDefaultImageFileName(isGallerySave: isGallerySave), + defaultImageFileNamePrefix); + } + }); + } + + test('returns default file name prefix when saving to gallery', () { + // The gallery app handles name conflicts. + expect(getDefaultImageFileName(isGallerySave: true), + defaultImageFileNamePrefix); + }); + + test( + 'returns default file name prefix for system save dialog on macOS and Windows', + () { + for (final platform in {TargetPlatform.macOS, TargetPlatform.windows}) { + debugDefaultTargetPlatformOverride = platform; + + expect(getDefaultImageFileName(isGallerySave: false), + defaultImageFileNamePrefix); + } + }); + + test('returns unique file name for system save dialog image on Linux', () { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + + final imageFileName = getDefaultImageFileName(isGallerySave: false); + expect(imageFileName, isNot(equals(defaultImageFileNamePrefix))); + + final imageFileName2 = getDefaultImageFileName(isGallerySave: false); + expect( + imageFileName2, + isNot(equals(imageFileName)), + reason: 'File name should be unique', + ); + + final imageFileName3 = getDefaultImageFileName(isGallerySave: false); + expect( + imageFileName3, + isNot(equals(imageFileName2)), + reason: 'File name should be unique', + ); + expect( + imageFileName3, + isNot(equals(imageFileName)), + reason: 'File name should be unique', + ); + }); + + test('returns unique file name for other platforms', () { + final imageFileName = getDefaultImageFileName(isGallerySave: false); + expect(imageFileName, isNot(equals(defaultImageFileNamePrefix))); + + final imageFileName2 = getDefaultImageFileName(isGallerySave: false); + expect( + imageFileName2, + isNot(equals(imageFileName)), + reason: 'File name should be unique', + ); + + final imageFileName3 = getDefaultImageFileName(isGallerySave: false); + expect( + imageFileName3, + isNot(equals(imageFileName2)), + reason: 'File name should be unique', + ); + expect( + imageFileName3, + isNot(equals(imageFileName)), + reason: 'File name should be unique', + ); + }); + }); + + group('shouldSaveToGallery', () { + late MockQuillNativeBridge mockQuillNativeBridge; + + void mockGallerySaveSupported(bool isSupported) => + when(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImageToGallery)) + .thenAnswer((_) async => isSupported); + + void mockImageSaveSupported(bool isSupported) => + when(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)) + .thenAnswer((_) async => isSupported); + + setUp(() { + mockQuillNativeBridge = MockQuillNativeBridge(); + QuillNativeProvider.instance = mockQuillNativeBridge; + }); + + test( + 'returns false if gallery save not supported regardless of prefersGallerySave', + () async { + mockGallerySaveSupported(false); + + for (final prefersGallerySave in {true, false}) { + final result = + await shouldSaveToGallery(prefersGallerySave: prefersGallerySave); + expect(result, isFalse); + verify(() => mockQuillNativeBridge.isSupported( + QuillNativeBridgeFeature.saveImageToGallery)).called(1); + verifyNever(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)); + } + }); + + test( + 'returns false when gallery save is not supported, regardless of if image save is supported', + () async { + mockGallerySaveSupported(false); + + for (final isImageSupported in {true, false}) { + mockImageSaveSupported(isImageSupported); + + final result = await shouldSaveToGallery(prefersGallerySave: true); + + expect(result, isFalse); + verify(() => mockQuillNativeBridge.isSupported( + QuillNativeBridgeFeature.saveImageToGallery)).called(1); + verifyNever(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)); + } + }); + + test( + 'returns true if gallery save is supported and prefersGallerySave is true', + () async { + for (final imageSaveSupported in {true, false}) { + mockGallerySaveSupported(true); + mockImageSaveSupported(imageSaveSupported); + + final result = await shouldSaveToGallery(prefersGallerySave: true); + + expect(result, isTrue); + verify(() => mockQuillNativeBridge.isSupported( + QuillNativeBridgeFeature.saveImageToGallery)).called(1); + verify(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)).called(1); + } + }); + + test( + 'returns true when gallery and image save are supported and prefersGallerySave is true', + () async { + mockGallerySaveSupported(true); + mockImageSaveSupported(true); + + final result = await shouldSaveToGallery(prefersGallerySave: true); + + expect(result, isTrue); + verify(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImageToGallery)).called(1); + verify(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)).called(1); + }); + + test( + 'returns false when gallery and image save are supported and prefersGallerySave is false', + () async { + mockGallerySaveSupported(true); + mockImageSaveSupported(true); + + final result = await shouldSaveToGallery(prefersGallerySave: false); + + expect(result, isFalse); + verify(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImageToGallery)).called(1); + verify(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)).called(1); + }); + + test( + 'returns false when gallery save is not supported and image save is supported regardless of prefersGallerySave', + () async { + mockGallerySaveSupported(false); + mockImageSaveSupported(true); + + for (final prefersGallerySave in {true, false}) { + final result = + await shouldSaveToGallery(prefersGallerySave: prefersGallerySave); + + expect(result, isFalse); + verify(() => mockQuillNativeBridge.isSupported( + QuillNativeBridgeFeature.saveImageToGallery)).called(1); + verifyNever(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)); + } + }); + + test( + 'returns true when gallery save supported and image save not supported regardless of prefersGallerySave', + () async { + mockGallerySaveSupported(true); + mockImageSaveSupported(false); + + for (final prefersGallerySave in {true, false}) { + final result = + await shouldSaveToGallery(prefersGallerySave: prefersGallerySave); + + expect(result, isTrue); + verify(() => mockQuillNativeBridge.isSupported( + QuillNativeBridgeFeature.saveImageToGallery)).called(1); + verify(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)); + } + }); + }); + + group('$ImageSaver', () { + test('default instance is $ImageSaver', () { + expect(ImageSaver.instance, isA()); + }); + + test('set the instance correctly', () { + expect(ImageSaver.instance, isNot(isA())); + + ImageSaver.instance = FakeImageSaver(); + expect(ImageSaver.instance, isA()); + }); + }); + + group('saveImage', () { + late MockQuillNativeBridge mockQuillNativeBridge; + late MockImageLoader mockImageLoader; + final imageSaver = ImageSaver.instance; + + void mockGallerySaveSupported(bool isSupported) => + when(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImageToGallery)) + .thenAnswer((_) async => isSupported); + + void mockImageSaveSupported(bool isSupported) => + when(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)) + .thenAnswer((_) async => isSupported); + + Future mockShouldSaveToGallery(bool shouldSaveToGalleryValue) async { + if (shouldSaveToGalleryValue) { + mockGallerySaveSupported(true); + mockImageSaveSupported(false); + } else { + mockGallerySaveSupported(false); + mockImageSaveSupported(true); + } + expect( + await shouldSaveToGallery(prefersGallerySave: false), + shouldSaveToGalleryValue, + reason: + 'calling shouldSaveToGallery should return the value specified by mockShouldSaveToGallery', + ); + } + + void mockLoadImageBytesValue(Uint8List? imageBytes) => + when(() => mockImageLoader.loadImageBytesFromImageProvider( + imageProvider: any(named: 'imageProvider'), + )).thenAnswer((_) async => imageBytes); + + setUp(() { + mockQuillNativeBridge = MockQuillNativeBridge(); + QuillNativeProvider.instance = mockQuillNativeBridge; + + mockImageLoader = MockImageLoader(); + ImageLoader.instance = mockImageLoader; + + mockGallerySaveSupported(false); + mockImageSaveSupported(false); + when(() => + mockQuillNativeBridge.saveImage(any(), + options: any(named: 'options'))).thenAnswer( + (_) async => const ImageSaveResult(blobUrl: null, filePath: null)); + + when(() => mockQuillNativeBridge.saveImageToGallery(any(), + options: any(named: 'options'))).thenAnswer((_) async {}); + mockLoadImageBytesValue(null); + }); + + setUpAll(() { + registerFallbackValue(Uint8List.fromList([])); + registerFallbackValue( + const ImageSaveOptions(fileExtension: '', name: '')); + registerFallbackValue( + const GalleryImageSaveOptions( + albumName: '', name: '', fileExtension: ''), + ); + registerFallbackValue(FakeImageProvider()); + }); + + test('throws $ArgumentError when image URL is empty', () async { + await expectLater( + imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '', + prefersGallerySave: false, + ), + throwsA(isA()), + ); + }); + + test('does not throw $ArgumentError when the image URL is not empty', + () async { + await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ); + await expectLater( + imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ), + completes, + ); + }); + + test('calls $ImageLoader to load the image bytes from the $ImageProvider', + () async { + await imageSaver.saveImage( + imageUrl: 'imageUrl', + imageProvider: FakeImageProvider(), + prefersGallerySave: false, + ); + verify( + () => mockImageLoader.loadImageBytesFromImageProvider( + imageProvider: any(named: 'imageProvider')), + ).called(1); + }); + + test( + 'returns null when image bytes are null or empty', + () async { + await mockShouldSaveToGallery(true); + + for (final imageBytes in {Uint8List.fromList([]), null}) { + mockLoadImageBytesValue(imageBytes); + + final result = await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ); + expect(result, isNull); + + verify( + () => mockImageLoader.loadImageBytesFromImageProvider( + imageProvider: any(named: 'imageProvider')), + ).called(1); + } + }, + ); + + test( + 'calls saveImageToGallery from $QuillNativeBridge when shouldSaveToGallery is true', + () async { + await mockShouldSaveToGallery(true); + + mockLoadImageBytesValue(Uint8List.fromList([1, 0, 1])); + await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ); + verify( + () => mockQuillNativeBridge.saveImageToGallery(any(), + options: any(named: 'options')), + ).called(1); + }, + ); + + test( + 'does not call saveImageToGallery from $QuillNativeBridge when shouldSaveToGallery is false', + () async { + await mockShouldSaveToGallery(false); + + mockLoadImageBytesValue(Uint8List.fromList([1, 0, 1])); + await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ); + verifyNever( + () => mockQuillNativeBridge.saveImageToGallery(any(), + options: any(named: 'options')), + ); + }, + ); + + test( + 'calls saveImageToGallery from $QuillNativeBridge when should save to the gallery and image bytes are not null', + () async { + await mockShouldSaveToGallery(true); + + mockLoadImageBytesValue(Uint8List.fromList([1, 2, 2])); + + final result = await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ); + expect( + result, + const SaveImageResult( + isGallerySave: true, + imageFilePath: null, + ), + ); + verify( + () => mockQuillNativeBridge.saveImageToGallery(any(), + options: any(named: 'options')), + ).called(1); + }, + ); + + test( + 'throws $GalleryImageSaveAccessDeniedException in case permission is denied', + () async { + await mockShouldSaveToGallery(true); + + mockLoadImageBytesValue(Uint8List.fromList([1, 2, 2])); + + final platformException = PlatformException(code: 'PERMISSION_DENIED'); + when(() => mockQuillNativeBridge.saveImageToGallery(any(), + options: any(named: 'options'))).thenThrow(platformException); + + await expectLater( + imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ), + throwsA( + isA().having( + (e) => e.message, 'message', platformException.toString()), + ), + ); + }); + + test( + 'rethrows the $PlatformException in case permission is denied on macOS in debug-builds only (known macOS issue)', + () async { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + + await mockShouldSaveToGallery(true); + + mockLoadImageBytesValue(Uint8List.fromList([1, 2, 2])); + + final platformException = PlatformException( + code: 'PERMISSION_DENIED', message: 'A known macOS issue'); + when(() => mockQuillNativeBridge.saveImageToGallery(any(), + options: any(named: 'options'))).thenThrow(platformException); + + await expectLater( + imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ), + throwsA( + isA() + .having((e) => e.code, 'code', platformException.code) + .having((e) => e.message, 'message', platformException.message) + .having((e) => e.details, 'details', platformException.details), + ), + ); + }, skip: kReleaseMode); + + test( + 'rethrows the $PlatformException from $QuillNativeBridge if not handled', + () async { + // Currently, that's the expected behavior but it is subject to changes for improvements. + // See https://github.com/FlutterQuill/quill-native-bridge/issues/2 + + await mockShouldSaveToGallery(true); + + mockLoadImageBytesValue(Uint8List.fromList([1, 2, 2])); + + final exception = PlatformException( + code: 'EXAMPLE_CODE_${DateTime.now().toIso8601String()}', + message: 'An example exception that is not handled', + ); + when(() => mockQuillNativeBridge.saveImageToGallery(any(), + options: any(named: 'options'))).thenThrow(exception); + + await expectLater( + imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ), + throwsA(equals(exception)), + ); + }, + ); + + test( + 'calls isSupported from $QuillNativeBridge to check if image save supported when gallery save skipped', + () async { + await mockShouldSaveToGallery(false); + + mockLoadImageBytesValue(Uint8List.fromList([1, 2, 2])); + + await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ); + + verify(() => mockQuillNativeBridge + .isSupported(QuillNativeBridgeFeature.saveImage)).called(1); + }, + ); + + test( + 'calls saveImage from $QuillNativeBridge when supported and should not use gallery save', + () async { + await mockShouldSaveToGallery(false); + + mockLoadImageBytesValue(Uint8List.fromList([1, 2, 2])); + + mockImageSaveSupported(true); + + await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ); + + verify(() => mockQuillNativeBridge.saveImage(any(), + options: any(named: 'options'))).called(1); + }, + ); + + test( + 'does not calls saveImage from $QuillNativeBridge when unsupported and should not use gallery save', + () async { + await mockShouldSaveToGallery(false); + + mockLoadImageBytesValue(Uint8List.fromList([1, 2, 2])); + + mockImageSaveSupported(false); + + try { + await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ); + } on StateError catch (_) { + // Skip since another test handles it + } + + verifyNever(() => mockQuillNativeBridge.saveImage(any(), + options: any(named: 'options'))); + }, + ); + + test( + 'passes the arugments correctly to saveImageToGallery from $QuillNativeBridge', + () async { + for (final imageUrl in { + 'path/to/file.png', + 'http://flutter-quill.org/file.png' + }) { + await mockShouldSaveToGallery(true); + + final imageBytes = Uint8List.fromList([1, 0, 1]); + mockLoadImageBytesValue(imageBytes); + + await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: imageUrl, + prefersGallerySave: false, + ); + + final imageFileExtension = + extractImageFileExtensionFromImageSource(imageUrl); + final imageName = extractImageNameFromImageSource(imageUrl); + + verify( + () => mockQuillNativeBridge.saveImageToGallery( + imageBytes, + options: GalleryImageSaveOptions( + name: imageName ?? getDefaultImageFileName(isGallerySave: true), + fileExtension: imageFileExtension, + albumName: null, + ), + ), + ).called(1); + } + }, + ); + + test( + 'passes the arugments correctly to saveImage from $QuillNativeBridge', + () async { + for (final imageUrl in { + 'path/to/file.png', + 'http://flutter-quill.org/file.png' + }) { + await mockShouldSaveToGallery(false); + + final imageBytes = Uint8List.fromList([1, 0, 1]); + mockLoadImageBytesValue(imageBytes); + + mockImageSaveSupported(true); + + await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: imageUrl, + prefersGallerySave: false, + ); + + final imageFileExtension = + extractImageFileExtensionFromImageSource(imageUrl); + final imageName = extractImageNameFromImageSource(imageUrl); + + verify( + () => mockQuillNativeBridge.saveImage( + imageBytes, + options: ImageSaveOptions( + name: + imageName ?? getDefaultImageFileName(isGallerySave: false), + fileExtension: imageFileExtension, + ), + ), + ).called(1); + } + }, + ); + + test( + 'returns the $SaveImageResult correctly for image save', + () async { + await mockShouldSaveToGallery(false); + + final imageBytes = Uint8List.fromList([1, 0, 1]); + mockLoadImageBytesValue(imageBytes); + + mockImageSaveSupported(true); + + const inputImagePath = 'path/to/example_file.png'; + + const savedImagePath = '/path/to/saved/example_file.png'; + + when( + () => mockQuillNativeBridge.saveImage(imageBytes, + options: any(named: 'options')), + ).thenAnswer((_) async => + const ImageSaveResult(filePath: savedImagePath, blobUrl: null)); + + final result = await imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: inputImagePath, + prefersGallerySave: false, + ); + + expect( + result, + const SaveImageResult( + imageFilePath: savedImagePath, isGallerySave: false), + ); + }, + ); + + test( + 'throws $StateError when both image and gallery unsupported', + () async { + await mockShouldSaveToGallery(false); + + mockLoadImageBytesValue(Uint8List.fromList([1, 2, 2])); + + mockImageSaveSupported(false); + + await expectLater( + imageSaver.saveImage( + imageProvider: FakeImageProvider(), + imageUrl: '/foo/bar', + prefersGallerySave: false, + ), + throwsA(isA().having((e) => e.message, 'message', + 'Image save is not handled on $defaultTargetPlatform')), + ); + }, + ); + }); +} + +class MockQuillNativeBridge extends Mock implements QuillNativeBridge {} + +class MockImageLoader extends Mock implements ImageLoader {} + +class FakeImageProvider extends ImageProvider { + @override + Future obtainKey(ImageConfiguration configuration) async => + UnimplementedError('Fake implementation of $ImageProvider'); +} + +class FakeImageSaver implements ImageSaver { + @override + Future saveImage( + {required String imageUrl, + required ImageProvider imageProvider, + required bool prefersGallerySave}) => + throw UnimplementedError('Fake implementation of $FakeImageSaver'); +} diff --git a/flutter_quill_extensions/test/editor/image/widgets/image_menu_test.dart b/flutter_quill_extensions/test/editor/image/widgets/image_menu_test.dart new file mode 100644 index 000000000..bf067f956 --- /dev/null +++ b/flutter_quill_extensions/test/editor/image/widgets/image_menu_test.dart @@ -0,0 +1,322 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill_extensions/src/common/utils/element_utils/element_utils.dart'; +import 'package:flutter_quill_extensions/src/editor/image/config/image_config.dart'; +import 'package:flutter_quill_extensions/src/editor/image/image_menu.dart'; +import 'package:flutter_quill_extensions/src/editor/image/image_save_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../quill_test_app.dart'; + +void main() { + group('$ImageOptionsMenu', () { + test('prefersGallerySave defaults to true', () { + final widget = ImageOptionsMenu( + controller: FakeQuillController(), + config: const QuillEditorImageEmbedConfig(), + imageProvider: FakeImageProvider(), + imageSource: 'imageSource', + readOnly: true, + imageSize: const ElementSize(200, 300), + ); + expect( + widget.prefersGallerySave, + isTrue, + reason: + 'The default of prefersGallerySave should be true for backward compatibility', + ); + }); + group('save image', () { + late MockImageSaver mockImageSaver; + final QuillController controller = FakeQuillController(); + + setUp(() { + mockImageSaver = MockImageSaver(); + ImageSaver.instance = mockImageSaver; + }); + + setUpAll(() { + registerFallbackValue(FakeImageProvider()); + }); + + Future pumpTargetWidget( + WidgetTester tester, { + String imageSource = 'http://flutter-quill.org/image.png', + ImageProvider? imageProvider, + bool prefersGallerySave = false, + }) async { + await tester.pumpWidget(QuillTestApp.withScaffold(ImageOptionsMenu( + controller: controller, + config: const QuillEditorImageEmbedConfig(), + imageProvider: imageProvider ?? FakeImageProvider(), + imageSource: imageSource, + readOnly: true, + imageSize: const ElementSize(200, 300), + prefersGallerySave: prefersGallerySave, + ))); + } + + Finder findTargetWidget() { + final saveButtonFinder = find.widgetWithIcon(ListTile, Icons.save); + expect(saveButtonFinder, findsOneWidget); + return saveButtonFinder; + } + + void mockSaveImageResult(SaveImageResult? result) => when( + () => mockImageSaver.saveImage( + imageUrl: any(named: 'imageUrl'), + imageProvider: any(named: 'imageProvider'), + prefersGallerySave: any(named: 'prefersGallerySave'), + ), + ).thenAnswer((_) async => result); + + void mockSaveImageThrows(Exception exception) => when( + () => mockImageSaver.saveImage( + imageUrl: any(named: 'imageUrl'), + imageProvider: any(named: 'imageProvider'), + prefersGallerySave: any(named: 'prefersGallerySave'), + ), + ).thenThrow(exception); + + Future tapTargetWidget(WidgetTester tester) async { + await tester.tap(findTargetWidget()); + await tester.pump(); + } + + testWidgets('calls saveImage from $ImageSaver', (tester) async { + mockSaveImageResult(null); + + await pumpTargetWidget(tester); + + await tapTargetWidget(tester); + + verify( + () => mockImageSaver.saveImage( + imageUrl: any(named: 'imageUrl'), + imageProvider: any(named: 'imageProvider'), + prefersGallerySave: any(named: 'prefersGallerySave'), + ), + ); + }); + + if (kIsWeb) { + testWidgets( + 'shows a success message when the image is downloaded on the web.', + (tester) async { + mockSaveImageResult(const SaveImageResult( + imageFilePath: null, isGallerySave: false)); + + await pumpTargetWidget(tester); + await tapTargetWidget(tester); + + final localizations = + tester.localizationsFromElement(ImageOptionsMenu); + + expect( + find.text(localizations.successImageDownloaded), + findsOneWidget, + ); + }, + ); + } + + testWidgets( + 'shows permission denied message only when permission is denied', + (tester) async { + mockSaveImageThrows(GalleryImageSaveAccessDeniedException()); + + await pumpTargetWidget(tester); + await tapTargetWidget(tester); + + final localizations = tester.localizationsFromElement(ImageOptionsMenu); + + expect( + find.text(localizations.saveImagePermissionDenied), + findsOneWidget, + ); + expect( + find.text(localizations.errorUnexpectedSavingImage), + findsNothing, + ); + expect( + find.text(localizations.successImageDownloaded), + findsNothing, + ); + expect( + find.text(localizations.successImageSavedGallery), + findsNothing, + ); + expect( + find.text(localizations.successImageSaved), + findsNothing, + ); + expect( + find.text(localizations.openFileLocation), + findsNothing, + ); + expect( + find.text(localizations.openFile), + findsNothing, + ); + expect( + find.text(localizations.openGallery), + findsNothing, + ); + }); + + testWidgets('shows error message when saving fails', (tester) async { + mockSaveImageResult(null); + + await pumpTargetWidget(tester); + await tapTargetWidget(tester); + + final localizations = tester.localizationsFromElement(ImageOptionsMenu); + + expect( + find.text(localizations.errorUnexpectedSavingImage), + findsOneWidget, + ); + + verify( + () => mockImageSaver.saveImage( + imageUrl: any(named: 'imageUrl'), + imageProvider: any(named: 'imageProvider'), + prefersGallerySave: any(named: 'prefersGallerySave'), + ), + ); + }); + + testWidgets('shows saved and open gallery on gallery save', + (tester) async { + mockSaveImageResult(const SaveImageResult( + imageFilePath: 'path/to/file', isGallerySave: true)); + + await pumpTargetWidget(tester); + + await tapTargetWidget(tester); + + final localizations = tester.localizationsFromElement(ImageOptionsMenu); + + expect( + find.text(localizations.successImageSavedGallery), + findsOneWidget, + ); + + expect( + find.text(localizations.openGallery), + findsOneWidget, + ); + }); + + for (final desktopPlatform in TargetPlatformVariant.desktop().values) { + testWidgets( + 'shows saved success image and open file path action on ${desktopPlatform.name}', + (tester) async { + debugDefaultTargetPlatformOverride = desktopPlatform; + + const savedImagePath = 'path/to/file'; + mockSaveImageResult(const SaveImageResult( + imageFilePath: savedImagePath, isGallerySave: false)); + + await pumpTargetWidget(tester); + await tapTargetWidget(tester); + + final localizations = + tester.localizationsFromElement(ImageOptionsMenu); + + expect( + find.text(localizations.successImageSaved), + findsOneWidget, + ); + + expect( + find.text(defaultTargetPlatform == TargetPlatform.macOS + ? localizations.openFile + : localizations.openFileLocation), + findsOneWidget, + ); + + debugDefaultTargetPlatformOverride = null; + }); + } + + for (final prefersGallerySave in {true, false}) { + testWidgets( + 'passes the arguments correctly to saveImage from $ImageSaver when prefersGallerySave is $prefersGallerySave', + (tester) async { + mockSaveImageResult( + const SaveImageResult(imageFilePath: null, isGallerySave: true), + ); + + const imageUrl = 'http://flutter-quill.org/image.webp'; + final imageProvider = AnotherFakeImageProvider(); + + await pumpTargetWidget( + tester, + imageSource: imageUrl, + prefersGallerySave: prefersGallerySave, + imageProvider: imageProvider, + ); + + await tapTargetWidget(tester); + + verify( + () => mockImageSaver.saveImage( + imageUrl: imageUrl, + imageProvider: imageProvider, + prefersGallerySave: prefersGallerySave), + ).called(1); + }); + } + + testWidgets('throws $StateError when save result is not handled', + (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; + + mockSaveImageResult( + const SaveImageResult(imageFilePath: null, isGallerySave: false)); + + Object? capturedException; + + await runZonedGuarded(() async { + await pumpTargetWidget(tester); + + await tapTargetWidget(tester); + }, (error, stackTrace) { + capturedException = error; + }); + + expect( + capturedException, + isA().having( + (e) => e.message, + 'message', + equals( + 'Image save result is not handled on $defaultTargetPlatform'), + )); + + debugDefaultTargetPlatformOverride = null; + }); + }); + }); +} + +class MockImageSaver extends Mock implements ImageSaver {} + +class FakeQuillController extends Fake implements QuillController {} + +class FakeImageProvider extends ImageProvider { + @override + Future obtainKey(ImageConfiguration configuration) async => + UnimplementedError('Fake implementation of $ImageProvider'); +} + +class AnotherFakeImageProvider extends ImageProvider { + @override + Future obtainKey(ImageConfiguration configuration) async => + UnimplementedError('Another fake implementation of $ImageProvider'); +} diff --git a/flutter_quill_extensions/test/quill_test_app.dart b/flutter_quill_extensions/test/quill_test_app.dart new file mode 100644 index 000000000..64f53ad16 --- /dev/null +++ b/flutter_quill_extensions/test/quill_test_app.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill/internal.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class QuillTestApp extends StatelessWidget { + QuillTestApp({ + required this.home, + required this.scaffoldBody, + super.key, + }) { + if (home != null && scaffoldBody != null) { + throw ArgumentError('Either the home or scaffoldBody must be null'); + } + } + + factory QuillTestApp.withScaffold(Widget body) => + QuillTestApp(home: null, scaffoldBody: body); + + factory QuillTestApp.home(Widget home) => + QuillTestApp(home: home, scaffoldBody: null); + + final Widget? home; + final Widget? scaffoldBody; + + @override + Widget build(BuildContext context) { + return MaterialApp( + localizationsDelegates: FlutterQuillLocalizations.localizationsDelegates, + supportedLocales: FlutterQuillLocalizations.supportedLocales, + home: home ?? + Scaffold( + body: scaffoldBody, + ), + ); + } +} + +extension LocalizationsExt on WidgetTester { + FlutterQuillLocalizations localizationsFromElement(Type type) => + (element(find.byType(type)) as BuildContext).loc; +} diff --git a/lib/internal.dart b/lib/internal.dart index ff99bced5..772cbdd86 100644 --- a/lib/internal.dart +++ b/lib/internal.dart @@ -11,6 +11,7 @@ library; import 'package:meta/meta.dart' show experimental; export 'src/common/utils/platform.dart'; +export 'src/common/utils/quill_native_provider.dart'; export 'src/common/utils/string.dart'; export 'src/common/utils/widgets.dart'; export 'src/document/nodes/leaf.dart'; diff --git a/lib/src/common/utils/platform.dart b/lib/src/common/utils/platform.dart index 8575de312..7d673469b 100644 --- a/lib/src/common/utils/platform.dart +++ b/lib/src/common/utils/platform.dart @@ -3,7 +3,8 @@ import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform, kDebugMode, kIsWeb; import 'package:flutter/material.dart'; -import 'package:quill_native_bridge/quill_native_bridge.dart'; + +import 'quill_native_provider.dart'; // Android @@ -26,7 +27,7 @@ Future isIOSSimulator() async { return false; } - return await QuillNativeBridge.isIOSSimulator(); + return await QuillNativeProvider.instance.isIOSSimulator(); } // Mobile diff --git a/lib/src/common/utils/quill_native_provider.dart b/lib/src/common/utils/quill_native_provider.dart new file mode 100644 index 000000000..726573adf --- /dev/null +++ b/lib/src/common/utils/quill_native_provider.dart @@ -0,0 +1,19 @@ +import 'package:meta/meta.dart'; +import 'package:quill_native_bridge/quill_native_bridge.dart'; + +export 'package:quill_native_bridge/quill_native_bridge.dart'; + +@visibleForTesting +typedef DefaultQuillNativeBridge = QuillNativeBridge; + +abstract final class QuillNativeProvider { + static QuillNativeBridge _instance = DefaultQuillNativeBridge(); + + static QuillNativeBridge get instance => _instance; + + /// Allows overriding the instance for testing. + /// Pass `null` to restore the default instance. + @visibleForTesting + static set instance(QuillNativeBridge? newInstance) => + _instance = newInstance ?? DefaultQuillNativeBridge(); +} diff --git a/lib/src/editor/widgets/text/text_line.dart b/lib/src/editor/widgets/text/text_line.dart index 94629ab91..0de0ad641 100644 --- a/lib/src/editor/widgets/text/text_line.dart +++ b/lib/src/editor/widgets/text/text_line.dart @@ -6,7 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher_string.dart' show launchUrlString; +import 'package:url_launcher/url_launcher.dart'; import '../../../../flutter_quill.dart'; import '../../../common/utils/color.dart'; @@ -612,7 +612,7 @@ class _TextLineState extends State { } Future _launchUrl(String url) async { - await launchUrlString(url); + await launchUrl(Uri.parse(url)); } void _tapNodeLink(Node node) { diff --git a/lib/src/editor_toolbar_controller_shared/clipboard/default_clipboard_service.dart b/lib/src/editor_toolbar_controller_shared/clipboard/default_clipboard_service.dart index 6ce39da24..420c9aade 100644 --- a/lib/src/editor_toolbar_controller_shared/clipboard/default_clipboard_service.dart +++ b/lib/src/editor_toolbar_controller_shared/clipboard/default_clipboard_service.dart @@ -2,9 +2,8 @@ import 'dart:io' as io; import 'package:flutter/foundation.dart'; import 'package:meta/meta.dart' show experimental; -import 'package:quill_native_bridge/quill_native_bridge.dart' - show QuillNativeBridge, QuillNativeBridgeFeature; +import '../../common/utils/quill_native_provider.dart'; import 'clipboard_service.dart'; /// Default implementation of [ClipboardService] to support rich clipboard @@ -13,50 +12,50 @@ import 'clipboard_service.dart'; class DefaultClipboardService extends ClipboardService { @override Future getHtmlText() async { - if (!(await QuillNativeBridge.isSupported( - QuillNativeBridgeFeature.getClipboardHtml))) { + if (!(await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.getClipboardHtml))) { return null; } - return await QuillNativeBridge.getClipboardHtml(); + return await QuillNativeProvider.instance.getClipboardHtml(); } @override Future getImageFile() async { - if (!(await QuillNativeBridge.isSupported( - QuillNativeBridgeFeature.getClipboardImage))) { + if (!(await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.getClipboardImage))) { return null; } - return await QuillNativeBridge.getClipboardImage(); + return await QuillNativeProvider.instance.getClipboardImage(); } @override Future copyImage(Uint8List imageBytes) async { - if (!(await QuillNativeBridge.isSupported( - QuillNativeBridgeFeature.copyImageToClipboard))) { + if (!(await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.copyImageToClipboard))) { return; } - await QuillNativeBridge.copyImageToClipboard(imageBytes); + await QuillNativeProvider.instance.copyImageToClipboard(imageBytes); } @override Future getGifFile() async { - if (!(await QuillNativeBridge.isSupported( - QuillNativeBridgeFeature.getClipboardGif))) { + if (!(await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.getClipboardGif))) { return null; } - return QuillNativeBridge.getClipboardGif(); + return QuillNativeProvider.instance.getClipboardGif(); } Future _getClipboardFile({required String fileExtension}) async { - if (!(await QuillNativeBridge.isSupported( - QuillNativeBridgeFeature.getClipboardFiles))) { + if (!(await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.getClipboardFiles))) { return null; } if (kIsWeb) { // TODO: Can't read file with dart:io on the Web (See related https://github.com/FlutterQuill/quill-native-bridge/issues/6) return null; } - final filePaths = await QuillNativeBridge.getClipboardFiles(); + final filePaths = await QuillNativeProvider.instance.getClipboardFiles(); final filePath = filePaths.firstWhere( (filePath) => filePath.endsWith('.$fileExtension'), orElse: () => '', diff --git a/lib/src/l10n/generated/quill_localizations.dart b/lib/src/l10n/generated/quill_localizations.dart index 6588ca608..895592ceb 100644 --- a/lib/src/l10n/generated/quill_localizations.dart +++ b/lib/src/l10n/generated/quill_localizations.dart @@ -768,6 +768,54 @@ abstract class FlutterQuillLocalizations { /// In en, this message translates to: /// **'Insert video'** String get insertVideo; + + /// A generic error message shown when an image cannot be saved due to an unknown issue + /// + /// In en, this message translates to: + /// **'An unexpected error occurred while saving the image. Please try again.'** + String get errorUnexpectedSavingImage; + + /// Message shown when an image is successfully saved to the system gallery + /// + /// In en, this message translates to: + /// **'Image saved to your gallery.'** + String get successImageSavedGallery; + + /// Message shown on desktop when an image is successfully saved. The user is prompted to open the file location + /// + /// In en, this message translates to: + /// **'Image saved successfully.'** + String get successImageSaved; + + /// Message shown on web when an image is successfully downloaded + /// + /// In en, this message translates to: + /// **'Image downloaded successfully.'** + String get successImageDownloaded; + + /// Label for the button that opens the system gallery + /// + /// In en, this message translates to: + /// **'Open Gallery'** + String get openGallery; + + /// Label for the button that opens the file explorer to the file's location + /// + /// In en, this message translates to: + /// **'Open File Location'** + String get openFileLocation; + + /// Label for the button that opens the file + /// + /// In en, this message translates to: + /// **'Open File'** + String get openFile; + + /// Message shown when the app is unable to save an image because a required permission was denied or skipped + /// + /// In en, this message translates to: + /// **'Couldn’t save the image due to missing permission'** + String get saveImagePermissionDenied; } class _FlutterQuillLocalizationsDelegate diff --git a/lib/src/l10n/generated/quill_localizations_ar.dart b/lib/src/l10n/generated/quill_localizations_ar.dart index c44d75bed..a5528cd4a 100644 --- a/lib/src/l10n/generated/quill_localizations_ar.dart +++ b/lib/src/l10n/generated/quill_localizations_ar.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -302,4 +304,30 @@ class FlutterQuillLocalizationsAr extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_bg.dart b/lib/src/l10n/generated/quill_localizations_bg.dart index c98c1d651..6672e70a1 100644 --- a/lib/src/l10n/generated/quill_localizations_bg.dart +++ b/lib/src/l10n/generated/quill_localizations_bg.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,4 +306,30 @@ class FlutterQuillLocalizationsBg extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_bn.dart b/lib/src/l10n/generated/quill_localizations_bn.dart index a1e64090b..20224df79 100644 --- a/lib/src/l10n/generated/quill_localizations_bn.dart +++ b/lib/src/l10n/generated/quill_localizations_bn.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -310,4 +312,30 @@ class FlutterQuillLocalizationsBn extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_ca.dart b/lib/src/l10n/generated/quill_localizations_ca.dart index 8ff5dcd14..d158a0113 100644 --- a/lib/src/l10n/generated/quill_localizations_ca.dart +++ b/lib/src/l10n/generated/quill_localizations_ca.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -306,4 +308,30 @@ class FlutterQuillLocalizationsCa extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_cs.dart b/lib/src/l10n/generated/quill_localizations_cs.dart index 3dd277033..aeef9fd40 100644 --- a/lib/src/l10n/generated/quill_localizations_cs.dart +++ b/lib/src/l10n/generated/quill_localizations_cs.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,4 +306,30 @@ class FlutterQuillLocalizationsCs extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_da.dart b/lib/src/l10n/generated/quill_localizations_da.dart index 9634f9127..cdafb8745 100644 --- a/lib/src/l10n/generated/quill_localizations_da.dart +++ b/lib/src/l10n/generated/quill_localizations_da.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -302,4 +304,30 @@ class FlutterQuillLocalizationsDa extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_de.dart b/lib/src/l10n/generated/quill_localizations_de.dart index a831037e0..eb4745283 100644 --- a/lib/src/l10n/generated/quill_localizations_de.dart +++ b/lib/src/l10n/generated/quill_localizations_de.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -308,4 +310,30 @@ class FlutterQuillLocalizationsDe extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_el.dart b/lib/src/l10n/generated/quill_localizations_el.dart index 6139774b4..94c98ea3a 100644 --- a/lib/src/l10n/generated/quill_localizations_el.dart +++ b/lib/src/l10n/generated/quill_localizations_el.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -309,4 +311,30 @@ class FlutterQuillLocalizationsEl extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_en.dart b/lib/src/l10n/generated/quill_localizations_en.dart index d05b756a4..0c025f21d 100644 --- a/lib/src/l10n/generated/quill_localizations_en.dart +++ b/lib/src/l10n/generated/quill_localizations_en.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,6 +306,32 @@ class FlutterQuillLocalizationsEn extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } /// The translations for English, as used in the United States (`en_US`). diff --git a/lib/src/l10n/generated/quill_localizations_es.dart b/lib/src/l10n/generated/quill_localizations_es.dart index bba3e0065..f3ffbd5ed 100644 --- a/lib/src/l10n/generated/quill_localizations_es.dart +++ b/lib/src/l10n/generated/quill_localizations_es.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,4 +306,30 @@ class FlutterQuillLocalizationsEs extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_fa.dart b/lib/src/l10n/generated/quill_localizations_fa.dart index 00b29a112..f2b2820b9 100644 --- a/lib/src/l10n/generated/quill_localizations_fa.dart +++ b/lib/src/l10n/generated/quill_localizations_fa.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -305,4 +307,30 @@ class FlutterQuillLocalizationsFa extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_fr.dart b/lib/src/l10n/generated/quill_localizations_fr.dart index a7a120311..f53c1393c 100644 --- a/lib/src/l10n/generated/quill_localizations_fr.dart +++ b/lib/src/l10n/generated/quill_localizations_fr.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -310,4 +312,30 @@ class FlutterQuillLocalizationsFr extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_he.dart b/lib/src/l10n/generated/quill_localizations_he.dart index 9019c3bce..1da4f7773 100644 --- a/lib/src/l10n/generated/quill_localizations_he.dart +++ b/lib/src/l10n/generated/quill_localizations_he.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,4 +306,30 @@ class FlutterQuillLocalizationsHe extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_hi.dart b/lib/src/l10n/generated/quill_localizations_hi.dart index a0ba7d071..a5831500c 100644 --- a/lib/src/l10n/generated/quill_localizations_hi.dart +++ b/lib/src/l10n/generated/quill_localizations_hi.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -307,4 +309,30 @@ class FlutterQuillLocalizationsHi extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_hu.dart b/lib/src/l10n/generated/quill_localizations_hu.dart index 98a04b0df..824019c3e 100644 --- a/lib/src/l10n/generated/quill_localizations_hu.dart +++ b/lib/src/l10n/generated/quill_localizations_hu.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -309,4 +311,30 @@ class FlutterQuillLocalizationsHu extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_id.dart b/lib/src/l10n/generated/quill_localizations_id.dart index 661d8da4b..5c8232af8 100644 --- a/lib/src/l10n/generated/quill_localizations_id.dart +++ b/lib/src/l10n/generated/quill_localizations_id.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -307,4 +309,30 @@ class FlutterQuillLocalizationsId extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_it.dart b/lib/src/l10n/generated/quill_localizations_it.dart index 4a2600f63..9e499eae6 100644 --- a/lib/src/l10n/generated/quill_localizations_it.dart +++ b/lib/src/l10n/generated/quill_localizations_it.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -308,4 +310,30 @@ class FlutterQuillLocalizationsIt extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_ja.dart b/lib/src/l10n/generated/quill_localizations_ja.dart index 0601cc719..67b24dd5f 100644 --- a/lib/src/l10n/generated/quill_localizations_ja.dart +++ b/lib/src/l10n/generated/quill_localizations_ja.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -301,4 +303,30 @@ class FlutterQuillLocalizationsJa extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_km.dart b/lib/src/l10n/generated/quill_localizations_km.dart index 67e534e83..331b766ab 100644 --- a/lib/src/l10n/generated/quill_localizations_km.dart +++ b/lib/src/l10n/generated/quill_localizations_km.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -305,4 +307,30 @@ class FlutterQuillLocalizationsKm extends FlutterQuillLocalizations { @override String get insertVideo => 'áž”áž‰áŸ’áž…ážŒáž›ážœážžážŠáŸážąážŒ'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_ko.dart b/lib/src/l10n/generated/quill_localizations_ko.dart index 93823e6dc..5a5321ef2 100644 --- a/lib/src/l10n/generated/quill_localizations_ko.dart +++ b/lib/src/l10n/generated/quill_localizations_ko.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -301,4 +303,30 @@ class FlutterQuillLocalizationsKo extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_ku.dart b/lib/src/l10n/generated/quill_localizations_ku.dart index 97c6a14c3..c60c12706 100644 --- a/lib/src/l10n/generated/quill_localizations_ku.dart +++ b/lib/src/l10n/generated/quill_localizations_ku.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -305,6 +307,32 @@ class FlutterQuillLocalizationsKu extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } /// The translations for Kurdish (`ku_CKB`). diff --git a/lib/src/l10n/generated/quill_localizations_ms.dart b/lib/src/l10n/generated/quill_localizations_ms.dart index 100384b72..f1c25634c 100644 --- a/lib/src/l10n/generated/quill_localizations_ms.dart +++ b/lib/src/l10n/generated/quill_localizations_ms.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -305,4 +307,30 @@ class FlutterQuillLocalizationsMs extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_ne.dart b/lib/src/l10n/generated/quill_localizations_ne.dart index 01c5cc9d5..6071f9fc7 100644 --- a/lib/src/l10n/generated/quill_localizations_ne.dart +++ b/lib/src/l10n/generated/quill_localizations_ne.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -308,4 +310,30 @@ class FlutterQuillLocalizationsNe extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_nl.dart b/lib/src/l10n/generated/quill_localizations_nl.dart index 6024f7a9b..88b144501 100644 --- a/lib/src/l10n/generated/quill_localizations_nl.dart +++ b/lib/src/l10n/generated/quill_localizations_nl.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -306,4 +308,30 @@ class FlutterQuillLocalizationsNl extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_no.dart b/lib/src/l10n/generated/quill_localizations_no.dart index 14a74b535..1ff4c31d7 100644 --- a/lib/src/l10n/generated/quill_localizations_no.dart +++ b/lib/src/l10n/generated/quill_localizations_no.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -306,4 +308,30 @@ class FlutterQuillLocalizationsNo extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_pl.dart b/lib/src/l10n/generated/quill_localizations_pl.dart index 2e6ddbc0f..5261aaea8 100644 --- a/lib/src/l10n/generated/quill_localizations_pl.dart +++ b/lib/src/l10n/generated/quill_localizations_pl.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -303,4 +305,30 @@ class FlutterQuillLocalizationsPl extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_pt.dart b/lib/src/l10n/generated/quill_localizations_pt.dart index 5ed690019..268a22cf1 100644 --- a/lib/src/l10n/generated/quill_localizations_pt.dart +++ b/lib/src/l10n/generated/quill_localizations_pt.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,6 +306,32 @@ class FlutterQuillLocalizationsPt extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } /// The translations for Portuguese, as used in Brazil (`pt_BR`). diff --git a/lib/src/l10n/generated/quill_localizations_ro.dart b/lib/src/l10n/generated/quill_localizations_ro.dart index 7e444c68b..d57860800 100644 --- a/lib/src/l10n/generated/quill_localizations_ro.dart +++ b/lib/src/l10n/generated/quill_localizations_ro.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -307,6 +309,32 @@ class FlutterQuillLocalizationsRo extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } /// The translations for Romanian Moldavian Moldovan, as used in Romania (`ro_RO`). diff --git a/lib/src/l10n/generated/quill_localizations_ru.dart b/lib/src/l10n/generated/quill_localizations_ru.dart index fbc5cdea3..ed6f67f2b 100644 --- a/lib/src/l10n/generated/quill_localizations_ru.dart +++ b/lib/src/l10n/generated/quill_localizations_ru.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -307,4 +309,30 @@ class FlutterQuillLocalizationsRu extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_sk.dart b/lib/src/l10n/generated/quill_localizations_sk.dart index 1a735bb4b..cc2b80722 100644 --- a/lib/src/l10n/generated/quill_localizations_sk.dart +++ b/lib/src/l10n/generated/quill_localizations_sk.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -308,4 +310,30 @@ class FlutterQuillLocalizationsSk extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_sr.dart b/lib/src/l10n/generated/quill_localizations_sr.dart index 57fb9a368..b37d239d7 100644 --- a/lib/src/l10n/generated/quill_localizations_sr.dart +++ b/lib/src/l10n/generated/quill_localizations_sr.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -306,4 +308,30 @@ class FlutterQuillLocalizationsSr extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_sv.dart b/lib/src/l10n/generated/quill_localizations_sv.dart index bc689f920..e2eb7254e 100644 --- a/lib/src/l10n/generated/quill_localizations_sv.dart +++ b/lib/src/l10n/generated/quill_localizations_sv.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,4 +306,30 @@ class FlutterQuillLocalizationsSv extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_sw.dart b/lib/src/l10n/generated/quill_localizations_sw.dart index b073303d1..ad7031faf 100644 --- a/lib/src/l10n/generated/quill_localizations_sw.dart +++ b/lib/src/l10n/generated/quill_localizations_sw.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -306,4 +308,30 @@ class FlutterQuillLocalizationsSw extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_th.dart b/lib/src/l10n/generated/quill_localizations_th.dart index a0604a307..07dcc58e7 100644 --- a/lib/src/l10n/generated/quill_localizations_th.dart +++ b/lib/src/l10n/generated/quill_localizations_th.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,4 +306,30 @@ class FlutterQuillLocalizationsTh extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_tk.dart b/lib/src/l10n/generated/quill_localizations_tk.dart index 21d0aec94..f97cb369b 100644 --- a/lib/src/l10n/generated/quill_localizations_tk.dart +++ b/lib/src/l10n/generated/quill_localizations_tk.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -302,4 +304,30 @@ class FlutterQuillLocalizationsTk extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_tr.dart b/lib/src/l10n/generated/quill_localizations_tr.dart index 2523b6647..d2f38c881 100644 --- a/lib/src/l10n/generated/quill_localizations_tr.dart +++ b/lib/src/l10n/generated/quill_localizations_tr.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -305,4 +307,30 @@ class FlutterQuillLocalizationsTr extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_uk.dart b/lib/src/l10n/generated/quill_localizations_uk.dart index 97565005f..5edb89581 100644 --- a/lib/src/l10n/generated/quill_localizations_uk.dart +++ b/lib/src/l10n/generated/quill_localizations_uk.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -308,4 +310,30 @@ class FlutterQuillLocalizationsUk extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_ur.dart b/lib/src/l10n/generated/quill_localizations_ur.dart index 1e32f7a20..0b55cfd58 100644 --- a/lib/src/l10n/generated/quill_localizations_ur.dart +++ b/lib/src/l10n/generated/quill_localizations_ur.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -308,4 +310,30 @@ class FlutterQuillLocalizationsUr extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_vi.dart b/lib/src/l10n/generated/quill_localizations_vi.dart index 49addf610..d87da3934 100644 --- a/lib/src/l10n/generated/quill_localizations_vi.dart +++ b/lib/src/l10n/generated/quill_localizations_vi.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -304,4 +306,30 @@ class FlutterQuillLocalizationsVi extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } diff --git a/lib/src/l10n/generated/quill_localizations_zh.dart b/lib/src/l10n/generated/quill_localizations_zh.dart index 185c8cce3..45bcb7a7f 100644 --- a/lib/src/l10n/generated/quill_localizations_zh.dart +++ b/lib/src/l10n/generated/quill_localizations_zh.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'quill_localizations.dart'; // ignore_for_file: type=lint @@ -301,6 +303,32 @@ class FlutterQuillLocalizationsZh extends FlutterQuillLocalizations { @override String get insertVideo => 'Insert video'; + + @override + String get errorUnexpectedSavingImage => + 'An unexpected error occurred while saving the image. Please try again.'; + + @override + String get successImageSavedGallery => 'Image saved to your gallery.'; + + @override + String get successImageSaved => 'Image saved successfully.'; + + @override + String get successImageDownloaded => 'Image downloaded successfully.'; + + @override + String get openGallery => 'Open Gallery'; + + @override + String get openFileLocation => 'Open File Location'; + + @override + String get openFile => 'Open File'; + + @override + String get saveImagePermissionDenied => + 'Couldn’t save the image due to missing permission'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/src/l10n/quill_en.arb b/lib/src/l10n/quill_en.arb index 9a0ffbcd1..71cc176d6 100644 --- a/lib/src/l10n/quill_en.arb +++ b/lib/src/l10n/quill_en.arb @@ -43,7 +43,7 @@ "alignRight": "Align right", "alignJustify": "Align justify", "@alignJustify": { - "description": "Justify the text over the full window width" + "description": "Justify the text over the full window width" }, "justifyWinWidth": "Justify win width", "textDirection": "Text direction", @@ -109,5 +109,37 @@ "cut": "Cut", "paste": "Paste", "insertTable": "Insert table", - "insertVideo": "Insert video" + "insertVideo": "Insert video", + "errorUnexpectedSavingImage": "An unexpected error occurred while saving the image. Please try again.", + "@errorUnexpectedSavingImage": { + "description": "A generic error message shown when an image cannot be saved due to an unknown issue" + }, + "successImageSavedGallery": "Image saved to your gallery.", + "@successImageSavedGallery": { + "description": "Message shown when an image is successfully saved to the system gallery" + }, + "successImageSaved": "Image saved successfully.", + "@successImageSaved": { + "description": "Message shown on desktop when an image is successfully saved. The user is prompted to open the file location" + }, + "successImageDownloaded": "Image downloaded successfully.", + "@successImageDownloaded": { + "description": "Message shown on web when an image is successfully downloaded" + }, + "openGallery": "Open Gallery", + "@openGallery": { + "description": "Label for the button that opens the system gallery" + }, + "openFileLocation": "Open File Location", + "@openFileLocation": { + "description": "Label for the button that opens the file explorer to the file's location" + }, + "openFile": "Open File", + "@openFile": { + "description": "Label for the button that opens the file" + }, + "saveImagePermissionDenied": "Couldn’t save the image due to missing permission", + "@saveImagePermissionDenied": { + "description": "Message shown when the app is unable to save an image because a required permission was denied or skipped" + } } diff --git a/lib/src/l10n/untranslated.json b/lib/src/l10n/untranslated.json index dcee357f7..d3232a1f9 100644 --- a/lib/src/l10n/untranslated.json +++ b/lib/src/l10n/untranslated.json @@ -1,177 +1,540 @@ { "ar": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "bg": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "bn": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ca": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "cs": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "da": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "de": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "el": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "en_US": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "es": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "fa": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "fr": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "he": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "hi": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "hu": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "id": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "it": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ja": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" + ], + + "km": [ + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ko": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ku": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ku_CKB": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ms": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ne": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "nl": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "no": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "pl": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "pt": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "pt_BR": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ro": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ro_RO": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ru": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "sk": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "sr": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "sv": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "sw": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "th": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "tk": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "tr": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "uk": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "ur": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "vi": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "zh": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "zh_CN": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ], "zh_HK": [ - "insertVideo" + "insertVideo", + "errorUnexpectedSavingImage", + "successImageSavedGallery", + "successImageSaved", + "successImageDownloaded", + "openGallery", + "openFileLocation", + "openFile", + "saveImagePermissionDenied" ] } diff --git a/pubspec.yaml b/pubspec.yaml index 77812f6a0..fb908b7e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,7 +52,7 @@ dependencies: # Plugins url_launcher: ^6.2.1 flutter_keyboard_visibility_temp_fork: ^0.1.1 - quill_native_bridge: ^10.7.11 + quill_native_bridge: ^11.0.0 dev_dependencies: flutter_lints: ^5.0.0 diff --git a/scripts/translations_check.dart b/scripts/translations_check.dart index 965130bbc..a8d582a87 100644 --- a/scripts/translations_check.dart +++ b/scripts/translations_check.dart @@ -18,7 +18,7 @@ import 'package:yaml/yaml.dart'; // This must be updated once add or remove some translation keys // if you update existing keys, no need to update it -const _expectedTranslationKeysLength = 101; +const _expectedTranslationKeysLength = 117; Future main(List args) async { final l10nYamlText = await File('l10n.yaml').readAsString(); diff --git a/test/common/utils/quill_native_provider_test.dart b/test/common/utils/quill_native_provider_test.dart new file mode 100644 index 000000000..f297f55f9 --- /dev/null +++ b/test/common/utils/quill_native_provider_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_quill/src/common/utils/quill_native_provider.dart'; +import 'package:test/test.dart'; + +void main() { + group('$QuillNativeProvider', () { + test('defaults to $DefaultQuillNativeBridge', () { + expect(QuillNativeProvider.instance, isA()); + }); + + test('set the instance correctly', () { + expect(QuillNativeProvider, isNot(isA<_FakeQuillNativeBridge>())); + + QuillNativeProvider.instance = _FakeQuillNativeBridge(); + expect(QuillNativeProvider.instance, isA<_FakeQuillNativeBridge>()); + }); + + test('passing null restores the default instance', () { + final fake = _FakeQuillNativeBridge(); + QuillNativeProvider.instance = fake; + + QuillNativeProvider.instance = null; + expect(QuillNativeProvider.instance, isA()); + }); + + test('isSupported from the instance delegates to the new provider instance', + () async { + final fake = _FakeQuillNativeBridge(); + + QuillNativeProvider.instance = fake; + for (final isSupported in {true, false}) { + fake.testIsSupported = isSupported; + + expect( + await QuillNativeProvider.instance + .isSupported(QuillNativeBridgeFeature.isIOSSimulator), + await fake.isSupported(QuillNativeBridgeFeature.isIOSSimulator), + ); + } + }); + }); +} + +class _FakeQuillNativeBridge extends QuillNativeBridge { + var testIsSupported = false; + @override + Future isSupported(QuillNativeBridgeFeature feature) async => + testIsSupported; +}