From 1e54c947faa25f0a7ba80dd29aa0435c830a8c31 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 03:51:51 -0800 Subject: [PATCH 01/11] chore: fix deprecations of Flutter 3.27.0 (#2416) * chore: migrate from withOpacity to withValues per https://docs.flutter.dev/release/breaking-changes/wide-gamut-framework#opacity * chore: avoid using the deprecated Color.value getter * chore(analysis): ignore unreachable_switch_default for backward compatibility * chore(example): update pubspec.lock using the latest Flutter stable (3.27.0) * docs(changelog): document the changes * chore(deps): improve intl constraint for compatibility --- CHANGELOG.md | 8 ++ analysis_options.yaml | 4 + example/pubspec.lock | 92 +++++++++---------- flutter_quill_extensions/CHANGELOG.md | 4 + .../analysis_options.yaml | 4 + lib/src/editor/editor.dart | 4 +- .../editor/style_widgets/checkbox_point.dart | 8 +- lib/src/editor/widgets/cursor.dart | 2 +- lib/src/editor/widgets/default_styles.dart | 8 +- lib/src/editor/widgets/float_cursor.dart | 2 +- lib/src/editor/widgets/link.dart | 4 +- lib/src/editor/widgets/text/text_block.dart | 2 +- lib/src/editor_toolbar_shared/color.dart | 14 ++- pubspec.yaml | 2 +- test/editor_toolbar_shared/color_test.dart | 36 ++++++++ 15 files changed, 131 insertions(+), 63 deletions(-) create mode 100644 test/editor_toolbar_shared/color_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index c5cba4038..396f4c7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- 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`. + ## [11.0.0-dev.14] - 2024-11-24 ### Changed diff --git a/analysis_options.yaml b/analysis_options.yaml index b6065426f..ee1ee99a9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,5 +1,9 @@ include: package:flutter_lints/flutter.yaml +analyzer: +# TODO: Included for backward compatibility, remove when the minimum Dart SDK is 3.6.0 + errors: + unreachable_switch_default: ignore linter: rules: always_declare_return_types: true diff --git a/example/pubspec.lock b/example/pubspec.lock index bb29dba5d..ff2944691 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,18 +13,18 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" characters: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" collection: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.1" ffi: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: b2b91daf8a68ecfa4a01b778a6f52edef9b14ecd506e771488ea0f2e0784198b + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+2" file_selector_macos: dependency: transitive description: @@ -222,10 +222,10 @@ packages: dependency: transitive description: name: flutter_quill_delta_from_html - sha256: "288f879bd11f9b6857868e7b198e69918530bd63d196ead6d8a9ee780b4b44d2" + sha256: "63873b5391b56daa999ce8fa7dd23dfd7d0417a70e00a647ba450f4a8988afd0" url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.3" flutter_quill_extensions: dependency: "direct main" description: @@ -302,10 +302,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "8faba09ba361d4b246dc0a17cb4289b3324c2b9f6db7b3d457ee69106a86bd32" + sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e url: "https://pub.dev" source: hosted - version: "0.8.12+17" + version: "0.8.12+18" image_picker_for_web: dependency: transitive description: @@ -366,18 +366,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -438,10 +438,10 @@ packages: dependency: "direct main" description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.0" photo_view: dependency: transitive description: @@ -470,58 +470,58 @@ packages: dependency: transitive description: name: quill_native_bridge_android - sha256: df94b59081dfded10052b61fd4bb80a013499029841d3f64b753b117c8db9dc3 + sha256: b75c7e6ede362a7007f545118e756b1f19053994144ec9eda932ce5e54a57569 url: "https://pub.dev" source: hosted - version: "0.0.1-dev.5" + version: "0.0.1+2" quill_native_bridge_ios: dependency: transitive description: name: quill_native_bridge_ios - sha256: "2bec0c95558eea85f422767c6ddab87ef774cf5fe4222e295a436a169d314ecc" + sha256: d23de3cd7724d482fe2b514617f8eedc8f296e120fb297368917ac3b59d8099f url: "https://pub.dev" source: hosted - version: "0.0.1-dev.6" + version: "0.0.1" quill_native_bridge_linux: dependency: transitive description: name: quill_native_bridge_linux - sha256: d226004634b69119007dffb95c88e06698644c1b3adfe984b78024129e223426 + sha256: "5fcc60cab2ab9079e0746941f05c5ca5fec85cc050b738c8c8b9da7c09da17eb" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.4" + version: "0.0.1" quill_native_bridge_macos: dependency: transitive description: name: quill_native_bridge_macos - sha256: "9bbe855cc2248f098732794a4861852fede97a384299b02646cb3dbfbc996a87" + sha256: "1c0631bd1e2eee765a8b06017c5286a4e829778f4585736e048eb67c97af8a77" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.5" + version: "0.0.1" quill_native_bridge_platform_interface: dependency: transitive description: name: quill_native_bridge_platform_interface - sha256: fdfc1001c9a152eb5789916f55da97a3de548ccd41fe3cdd0bfcc371bdf41d10 + sha256: "2d71b6c5106db0a4b1d788640d1b949ccdd0e570b5a5e0384f7b28be9630a94a" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.5" + version: "0.0.1" quill_native_bridge_web: dependency: transitive description: name: quill_native_bridge_web - sha256: ad8076ad740318b3cf6b9a043d3dc206bb2e1383e5371cbe09711b9c609b64a0 + sha256: e7e55047d68f1a88574c26dbe3f12988f49d07740590d8fc6280028bbde5b908 url: "https://pub.dev" source: hosted - version: "0.0.1-dev.6" + version: "0.0.1" quill_native_bridge_windows: dependency: transitive description: name: quill_native_bridge_windows - sha256: "62583f68dd426aa9a98a0582631780db44d0c6708625d27f84cc87bf02e49ea3" + sha256: "60e50d74238f22ceb43113d9a42b6627451dab9fc27f527b979a32051cf1da45" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.4" + version: "0.0.1" quiver: dependency: transitive description: @@ -563,10 +563,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -627,10 +627,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: @@ -643,10 +643,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -699,10 +699,10 @@ packages: dependency: transitive description: name: video_player_avfoundation - sha256: "0b146e5d82e886ff43e5a46c6bcbe390761b802864a6e2503eb612d69a405dfa" + sha256: "33224c19775fd244be2d6e3dbd8e1826ab162877bd61123bf71890772119a2b7" url: "https://pub.dev" source: hosted - version: "2.6.3" + version: "2.6.5" video_player_platform_interface: dependency: transitive description: @@ -723,10 +723,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "14.3.0" web: dependency: transitive description: @@ -739,10 +739,10 @@ packages: dependency: transitive description: name: win32 - sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "5.9.0" sdks: dart: ">=3.6.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 20d7492b3..02eeaa691 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Ignore `unreachable_switch_default` warning (introduced in Dart 3.6). + ## [11.0.0-dev.4] - 2024-11-24 > [!IMPORTANT] diff --git a/flutter_quill_extensions/analysis_options.yaml b/flutter_quill_extensions/analysis_options.yaml index 42a46376c..b294310e7 100644 --- a/flutter_quill_extensions/analysis_options.yaml +++ b/flutter_quill_extensions/analysis_options.yaml @@ -1,5 +1,9 @@ include: package:flutter_lints/flutter.yaml +analyzer: +# TODO: Included for backward compatibility, remove when the minimum Dart SDK is 3.6.0 + errors: + unreachable_switch_default: ignore linter: rules: always_declare_return_types: true diff --git a/lib/src/editor/editor.dart b/lib/src/editor/editor.dart index 8249750aa..2838a35c9 100644 --- a/lib/src/editor/editor.dart +++ b/lib/src/editor/editor.dart @@ -239,7 +239,7 @@ class QuillEditorState extends State cursorOpacityAnimates = true; cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; selectionColor = selectionTheme.selectionColor ?? - cupertinoTheme.primaryColor.withOpacity(0.40); + cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0); @@ -249,7 +249,7 @@ class QuillEditorState extends State cursorOpacityAnimates = false; cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; selectionColor = selectionTheme.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); + theme.colorScheme.primary.withValues(alpha: 0.40); } final showSelectionToolbar = configurations.enableInteractiveSelection && diff --git a/lib/src/editor/style_widgets/checkbox_point.dart b/lib/src/editor/style_widgets/checkbox_point.dart index f79a5f4be..1c98549cc 100644 --- a/lib/src/editor/style_widgets/checkbox_point.dart +++ b/lib/src/editor/style_widgets/checkbox_point.dart @@ -35,15 +35,15 @@ class QuillCheckboxPointState extends State { final fillColor = widget.value ? (widget.enabled ? theme.colorScheme.primary - : theme.colorScheme.onSurface.withOpacity(0.5)) + : theme.colorScheme.onSurface.withValues(alpha: 0.5)) : theme.colorScheme.surface; final borderColor = widget.value ? (widget.enabled ? theme.colorScheme.primary - : theme.colorScheme.onSurface.withOpacity(0)) + : theme.colorScheme.onSurface.withValues(alpha: 0)) : (widget.enabled - ? theme.colorScheme.onSurface.withOpacity(0.5) - : theme.colorScheme.onSurface.withOpacity(0.3)); + ? theme.colorScheme.onSurface.withValues(alpha: 0.5) + : theme.colorScheme.onSurface.withValues(alpha: 0.3)); final child = Container( alignment: AlignmentDirectional.centerEnd, padding: EdgeInsetsDirectional.only(end: widget.size / 2), diff --git a/lib/src/editor/widgets/cursor.dart b/lib/src/editor/widgets/cursor.dart index e89df9e81..de126e909 100644 --- a/lib/src/editor/widgets/cursor.dart +++ b/lib/src/editor/widgets/cursor.dart @@ -232,7 +232,7 @@ class CursorCont extends ChangeNotifier { } void _onColorTick() { - color.value = _style.color.withOpacity(_blinkOpacityController.value); + color.value = _style.color.withValues(alpha: _blinkOpacityController.value); blink.value = show.value && _blinkOpacityController.value > 0; } } diff --git a/lib/src/editor/widgets/default_styles.dart b/lib/src/editor/widgets/default_styles.dart index 0d55366e7..a0f1b084e 100644 --- a/lib/src/editor/widgets/default_styles.dart +++ b/lib/src/editor/widgets/default_styles.dart @@ -260,7 +260,7 @@ class DefaultStyles { final inlineCodeStyle = TextStyle( fontSize: 14, - color: themeData.colorScheme.primary.withOpacity(0.8), + color: themeData.colorScheme.primary.withValues(alpha: 0.8), fontFamily: fontFamily, ); @@ -424,7 +424,7 @@ class DefaultStyles { defaultTextStyle.style.copyWith( fontSize: 20, height: 1.5, - color: Colors.grey.withOpacity(0.6), + color: Colors.grey.withValues(alpha: 0.6), ), baseHorizontalSpacing, VerticalSpacing.zero, @@ -439,7 +439,7 @@ class DefaultStyles { null, ), quote: DefaultTextBlockStyle( - TextStyle(color: baseStyle.color!.withOpacity(0.6)), + TextStyle(color: baseStyle.color!.withValues(alpha: 0.6)), baseHorizontalSpacing, baseVerticalSpacing, const VerticalSpacing(6, 2), @@ -451,7 +451,7 @@ class DefaultStyles { ), code: DefaultTextBlockStyle( TextStyle( - color: Colors.blue.shade900.withOpacity(0.9), + color: Colors.blue.shade900.withValues(alpha: 0.9), fontFamily: fontFamily, fontSize: 13, height: 1.15, diff --git a/lib/src/editor/widgets/float_cursor.dart b/lib/src/editor/widgets/float_cursor.dart index 7132e5d8a..f5849ae01 100644 --- a/lib/src/editor/widgets/float_cursor.dart +++ b/lib/src/editor/widgets/float_cursor.dart @@ -21,7 +21,7 @@ class FloatingCursorPainter { void paint(Canvas canvas) { final floatingCursorRect = this.floatingCursorRect; - final floatingCursorColor = style.color.withOpacity(0.75); + final floatingCursorColor = style.color.withValues(alpha: 0.75); if (floatingCursorRect == null) return; canvas.drawRRect( RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius), diff --git a/lib/src/editor/widgets/link.dart b/lib/src/editor/widgets/link.dart index 5e730d2e0..9ef819af4 100644 --- a/lib/src/editor/widgets/link.dart +++ b/lib/src/editor/widgets/link.dart @@ -209,7 +209,7 @@ class _CupertinoAction extends StatelessWidget { Icon( icon, size: theme.iconTheme.size, - color: theme.colorScheme.onSurface.withOpacity(0.75), + color: theme.colorScheme.onSurface.withValues(alpha: 0.75), ) ], ), @@ -267,7 +267,7 @@ class _MaterialAction extends StatelessWidget { leading: Icon( icon, size: theme.iconTheme.size, - color: theme.colorScheme.onSurface.withOpacity(0.75), + color: theme.colorScheme.onSurface.withValues(alpha: 0.75), ), title: Text(title), onTap: onPressed, diff --git a/lib/src/editor/widgets/text/text_block.dart b/lib/src/editor/widgets/text/text_block.dart index 6d2ca5473..09d170823 100644 --- a/lib/src/editor/widgets/text/text_block.dart +++ b/lib/src/editor/widgets/text/text_block.dart @@ -290,7 +290,7 @@ class EditableTextBlock extends StatelessWidget { return null; } return defaultStyles.code!.style.copyWith( - color: defaultStyles.code!.style.color!.withOpacity(0.4), + color: defaultStyles.code!.style.color!.withValues(alpha: 0.4), ); }(), width: () { diff --git a/lib/src/editor_toolbar_shared/color.dart b/lib/src/editor_toolbar_shared/color.dart index 653e842e9..14e2c31be 100644 --- a/lib/src/editor_toolbar_shared/color.dart +++ b/lib/src/editor_toolbar_shared/color.dart @@ -17,6 +17,18 @@ Color hexToColor(String? hexString) { return Color(int.tryParse(buffer.toString(), radix: 16) ?? 0xFF000000); } +// Without the hash sign (`#`). String colorToHex(Color color) { - return color.value.toRadixString(16).padLeft(8, '0').toUpperCase(); + int floatToInt8(double x) => (x * 255.0).round() & 0xff; + + final alpha = floatToInt8(color.a); + final red = floatToInt8(color.r); + final green = floatToInt8(color.g); + final blue = floatToInt8(color.b); + + return '${alpha.toRadixString(16).padLeft(2, '0')}' + '${red.toRadixString(16).padLeft(2, '0')}' + '${green.toRadixString(16).padLeft(2, '0')}' + '${blue.toRadixString(16).padLeft(2, '0')}' + .toUpperCase(); } diff --git a/pubspec.yaml b/pubspec.yaml index 863b52833..77812f6a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: quiver: ^3.2.0 meta: ^1.7.0 html: ^0.15.0 - intl: ^0.19.0 + intl: '>=0.19.0 <0.21.0' flutter_colorpicker: ^1.1.0 diff --git a/test/editor_toolbar_shared/color_test.dart b/test/editor_toolbar_shared/color_test.dart new file mode 100644 index 000000000..b58cafb68 --- /dev/null +++ b/test/editor_toolbar_shared/color_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/src/editor_toolbar_shared/color.dart'; +import 'package:test/test.dart'; + +void main() { + test('colorToHex converts to hex correctly', () { + const testCases = [ + _ColorTestCase(color: Color(0xFFFF0000), expectedHex: 'FFFF0000'), + _ColorTestCase(color: Color(0xFF00FF00), expectedHex: 'FF00FF00'), + _ColorTestCase(color: Color(0xFF0000FF), expectedHex: 'FF0000FF'), + _ColorTestCase(color: Color(0x00000000), expectedHex: '00000000'), + _ColorTestCase( + color: Color(0x80FFFFFF), + expectedHex: '80FFFFFF'), // 50% transparent white + _ColorTestCase(color: Color(0x12345678), expectedHex: '12345678'), + _ColorTestCase(color: Color(0xFF000000), expectedHex: 'FF000000'), + _ColorTestCase(color: Color(0xFFFFFFFF), expectedHex: 'FFFFFFFF'), + _ColorTestCase(color: Colors.black, expectedHex: 'FF000000'), + _ColorTestCase(color: Colors.white, expectedHex: 'FFFFFFFF'), + _ColorTestCase(color: Colors.transparent, expectedHex: '00000000'), + ]; + for (final testCase in testCases) { + expect(colorToHex(testCase.color), equals(testCase.expectedHex)); + } + }); +} + +class _ColorTestCase { + const _ColorTestCase({ + required this.color, + required this.expectedHex, + }); + + final Color color; + final String expectedHex; +} From a86f9bcbc2d155a198975bd81c9e6e48a528875e Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 04:40:41 -0800 Subject: [PATCH 02/11] feat(flutter_quill_extensions): improve image save (#2403) * chore: migrate quill_native_bridge to 11.0.0 * feat: fix broken image save functionality in flutter_quill_extensions, replace gal and gal_linux with quill_native_bridge * docs(changelog): update to reflect the change * chore(l10n): improve success and error messages * chore: avoid using url_launcher_string.dart, improve CHANGELOG.md changes * feat: show permission denied message, addressing https://github.com/singerdmx/flutter-quill/pull/2403#discussion_r1866156795 * test: minor changes for testing, moving the ImageSaver global instance to static instance inside ImageSaver class * chore: remove http and cross_file dependencies from flutter_quill_extensions * fix: fix Windows broken file separator, restore path package (remove custom implementation), addressing https://github.com/singerdmx/flutter-quill/pull/2403#discussion_r1867364560 and https://github.com/singerdmx/flutter-quill/pull/2403#discussion_r1865998051 * chore(l10n): add 'Open File' in quill_en.arb * fix: open the directory of the saved image file on desktop platforms other than Windows * chore: update the default of prefersGallerySave back to true, add test to ensure not accidentally changed * chore(example): Update minimum supported SDK version to Flutter 3.24/Dart 3.5 Due to adding Swift package manager integration to the iOS and macOS example * docs: document ImageSaver.saveImage --- .github/workflows/main.yml | 6 +- CHANGELOG.md | 10 + example/.gitignore | 2 + .../android/app/src/main/AndroidManifest.xml | 6 + example/ios/Runner.xcodeproj/project.pbxproj | 20 + .../xcshareddata/xcschemes/Runner.xcscheme | 19 + .../Flutter/GeneratedPluginRegistrant.swift | 2 - example/macos/Podfile.lock | 34 +- .../macos/Runner.xcodeproj/project.pbxproj | 40 +- .../xcshareddata/xcschemes/Runner.xcscheme | 19 + example/macos/Runner/AppDelegate.swift | 4 + .../macos/Runner/DebugProfile.entitlements | 2 +- example/macos/Runner/Release.entitlements | 2 +- example/pubspec.lock | 20 +- example/pubspec.yaml | 4 +- .../flutter/generated_plugin_registrant.cc | 3 - .../windows/flutter/generated_plugins.cmake | 1 - flutter_quill_extensions/CHANGELOG.md | 7 + flutter_quill_extensions/README.md | 11 +- .../lib/src/common/utils/utils.dart | 75 -- .../src/editor/image/image_load_utils.dart | 36 + .../lib/src/editor/image/image_menu.dart | 147 ++- .../src/editor/image/image_save_utils.dart | 254 +++++ flutter_quill_extensions/pubspec.yaml | 9 +- .../pubspec_overrides.yaml | 4 + .../editor/image/image_save_utils_test.dart | 958 ++++++++++++++++++ .../editor/image/widgets/image_menu_test.dart | 322 ++++++ .../test/quill_test_app.dart | 42 + lib/internal.dart | 1 + lib/src/common/utils/platform.dart | 5 +- .../common/utils/quill_native_provider.dart | 19 + lib/src/editor/widgets/text/text_line.dart | 4 +- .../clipboard/default_clipboard_service.dart | 33 +- .../l10n/generated/quill_localizations.dart | 48 + .../generated/quill_localizations_ar.dart | 28 + .../generated/quill_localizations_bg.dart | 28 + .../generated/quill_localizations_bn.dart | 28 + .../generated/quill_localizations_ca.dart | 28 + .../generated/quill_localizations_cs.dart | 28 + .../generated/quill_localizations_da.dart | 28 + .../generated/quill_localizations_de.dart | 28 + .../generated/quill_localizations_el.dart | 28 + .../generated/quill_localizations_en.dart | 28 + .../generated/quill_localizations_es.dart | 28 + .../generated/quill_localizations_fa.dart | 28 + .../generated/quill_localizations_fr.dart | 28 + .../generated/quill_localizations_he.dart | 28 + .../generated/quill_localizations_hi.dart | 28 + .../generated/quill_localizations_hu.dart | 28 + .../generated/quill_localizations_id.dart | 28 + .../generated/quill_localizations_it.dart | 28 + .../generated/quill_localizations_ja.dart | 28 + .../generated/quill_localizations_km.dart | 28 + .../generated/quill_localizations_ko.dart | 28 + .../generated/quill_localizations_ku.dart | 28 + .../generated/quill_localizations_ms.dart | 28 + .../generated/quill_localizations_ne.dart | 28 + .../generated/quill_localizations_nl.dart | 28 + .../generated/quill_localizations_no.dart | 28 + .../generated/quill_localizations_pl.dart | 28 + .../generated/quill_localizations_pt.dart | 28 + .../generated/quill_localizations_ro.dart | 28 + .../generated/quill_localizations_ru.dart | 28 + .../generated/quill_localizations_sk.dart | 28 + .../generated/quill_localizations_sr.dart | 28 + .../generated/quill_localizations_sv.dart | 28 + .../generated/quill_localizations_sw.dart | 28 + .../generated/quill_localizations_th.dart | 28 + .../generated/quill_localizations_tk.dart | 28 + .../generated/quill_localizations_tr.dart | 28 + .../generated/quill_localizations_uk.dart | 28 + .../generated/quill_localizations_ur.dart | 28 + .../generated/quill_localizations_vi.dart | 28 + .../generated/quill_localizations_zh.dart | 28 + lib/src/l10n/quill_en.arb | 36 +- lib/src/l10n/untranslated.json | 451 ++++++++- pubspec.yaml | 2 +- scripts/translations_check.dart | 2 +- .../utils/quill_native_provider_test.dart | 48 + 79 files changed, 3537 insertions(+), 291 deletions(-) create mode 100644 flutter_quill_extensions/lib/src/editor/image/image_load_utils.dart create mode 100644 flutter_quill_extensions/lib/src/editor/image/image_save_utils.dart create mode 100644 flutter_quill_extensions/pubspec_overrides.yaml create mode 100644 flutter_quill_extensions/test/editor/image/image_save_utils_test.dart create mode 100644 flutter_quill_extensions/test/editor/image/widgets/image_menu_test.dart create mode 100644 flutter_quill_extensions/test/quill_test_app.dart create mode 100644 lib/src/common/utils/quill_native_provider.dart create mode 100644 test/common/utils/quill_native_provider_test.dart 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; +} From 960f84c9834b7dbdb8bf4b9d0ace63cfe8d6cfe4 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 15:42:02 +0300 Subject: [PATCH 03/11] chore(release): prepare to publish 11.0.0-dev.15 --- CHANGELOG.md | 5 ++++- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f9784c5b..ca221a2ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.0.0-dev.15] - 2024-12-13 + ### Added - New localization strings for the image save functionality [#2403](https://github.com/singerdmx/flutter-quill/pull/2403). @@ -151,7 +153,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Apple-specific font dependency for subscript and superscript functionality from the example. - **BREAKING**: The [`super_clipboard`](https://pub.dev/packages/super_clipboard) plugin, To restore legacy behavior for `super_clipboard`, use [`flutter_quill_extensions`](https://pub.dev/packages/flutter_quill_extensions) package and `FlutterQuillExtensions.useSuperClipboardPlugin()`. -[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.14...HEAD +[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.15...HEAD +[11.0.0-dev.15]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.14...v11.0.0-dev.15 [11.0.0-dev.14]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.13...v11.0.0-dev.14 [11.0.0-dev.13]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.12...v11.0.0-dev.13 [11.0.0-dev.12]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.11...v11.0.0-dev.12 diff --git a/example/pubspec.lock b/example/pubspec.lock index 2d6c3d4cf..38ffef72a 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -217,7 +217,7 @@ packages: path: ".." relative: true source: path - version: "11.0.0-dev.14" + version: "11.0.0-dev.15" flutter_quill_delta_from_html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fb908b7e8..768d838a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: "A rich text editor built for Android, iOS, Web, and desktop platforms. It's the WYSIWYG editor and a Quill component for Flutter." -version: 11.0.0-dev.14 +version: 11.0.0-dev.15 homepage: https://github.com/singerdmx/flutter-quill/ repository: https://github.com/singerdmx/flutter-quill/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ From a616ebdac26cd7365584d890a4d81e1dc6de0db8 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 15:48:36 +0300 Subject: [PATCH 04/11] docs(changelog): include PR links to changes of https://github.com/singerdmx/flutter-quill/pull/2416 --- CHANGELOG.md | 6 +++--- flutter_quill_extensions/CHANGELOG.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca221a2ae..302a7963b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,11 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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: +- Avoid using deprecated APIs in Flutter 3.27.0 [#2416](https://github.com/singerdmx/flutter-quill/pull/2416): - 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`. +- Ignore `unreachable_switch_default` warning (introduced in Dart 3.6) [#2416](https://github.com/singerdmx/flutter-quill/pull/2416). +- Update `intl` dependency to support versions `0.19.0` and `0.20.0` [#2416](https://github.com/singerdmx/flutter-quill/pull/2416). ### Fixed diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index fcbde1e4d..7ca489538 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -13,7 +13,7 @@ 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). +- Ignore `unreachable_switch_default` warning (introduced in Dart 3.6) [#2416](https://github.com/singerdmx/flutter-quill/pull/2416). ### Removed From 56c4703bc82ed89a1b9c43950320a6f15b61f5a9 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 15:51:07 +0300 Subject: [PATCH 05/11] chore(release): prepare to publish flutter_quill_extensions 11.0.0-dev.6 --- flutter_quill_extensions/CHANGELOG.md | 2 ++ flutter_quill_extensions/pubspec.yaml | 4 ++-- flutter_quill_extensions/pubspec_overrides.yaml | 4 ---- 3 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 flutter_quill_extensions/pubspec_overrides.yaml diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 7ca489538..10f788ab2 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.0.0-dev.6] - 2024-12-13 + ### Changed - Rewrite the image save functionality with support for all platforms [#2403](https://github.com/singerdmx/flutter-quill/pull/2403). diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 03724288e..740e30384 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill to support loading images and videos -version: 11.0.0-dev.4 +version: 11.0.0-dev.6 homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ @@ -36,7 +36,7 @@ dependencies: universal_html: ^2.2.4 path: ^1.8.0 - flutter_quill: ^11.0.0-dev.3 + flutter_quill: ^11.0.0-dev.15 photo_view: ^0.15.0 # Plugins diff --git a/flutter_quill_extensions/pubspec_overrides.yaml b/flutter_quill_extensions/pubspec_overrides.yaml deleted file mode 100644 index 557e5b0c3..000000000 --- a/flutter_quill_extensions/pubspec_overrides.yaml +++ /dev/null @@ -1,4 +0,0 @@ -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 From 2a83b6bb92e81aacb8f6e5ac9a8c9e6048d78ba2 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 16:05:38 +0300 Subject: [PATCH 06/11] chore(example): update flutter_quill_extensions to the latest --- example/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 38ffef72a..a833c182b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -232,7 +232,7 @@ packages: path: "../flutter_quill_extensions" relative: true source: path - version: "11.0.0-dev.4" + version: "11.0.0-dev.6" flutter_quill_test: dependency: "direct dev" description: From 5468113110c390884f5fd40fcb6ad3881adccae0 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 05:58:55 -0800 Subject: [PATCH 07/11] chore: address Dart 3.6.0 analysis warnings (#2417) * chore(flutter_quill_extensions): address warnings of unreachable_switch_default instead of ignoring them * chore: address warnings of unreachable_switch_default instead of ignoring them --- CHANGELOG.md | 4 ++++ analysis_options.yaml | 4 ---- flutter_quill_extensions/CHANGELOG.md | 5 ++++ .../analysis_options.yaml | 4 ---- .../editor/image/widgets/image_resizer.dart | 23 ++++--------------- .../editor/raw_editor/raw_editor_state.dart | 5 ---- .../editor/widgets/text/text_selection.dart | 6 ----- 7 files changed, 14 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 302a7963b..299c7f937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Address warnings of `unreachable_switch_default` (introduced in Dart 3.6). + ## [11.0.0-dev.15] - 2024-12-13 ### Added diff --git a/analysis_options.yaml b/analysis_options.yaml index ee1ee99a9..b6065426f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,9 +1,5 @@ include: package:flutter_lints/flutter.yaml -analyzer: -# TODO: Included for backward compatibility, remove when the minimum Dart SDK is 3.6.0 - errors: - unreachable_switch_default: ignore linter: rules: always_declare_return_types: true diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 10f788ab2..06c044599 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Address warnings of `unreachable_switch_default` (introduced in Dart 3.6). +- Use `Slider.adaptive` for the image resize slider on Apple platforms for consistency with `CupertinoActionSheet`. + ## [11.0.0-dev.6] - 2024-12-13 ### Changed diff --git a/flutter_quill_extensions/analysis_options.yaml b/flutter_quill_extensions/analysis_options.yaml index b294310e7..42a46376c 100644 --- a/flutter_quill_extensions/analysis_options.yaml +++ b/flutter_quill_extensions/analysis_options.yaml @@ -1,9 +1,5 @@ include: package:flutter_lints/flutter.yaml -analyzer: -# TODO: Included for backward compatibility, remove when the minimum Dart SDK is 3.6.0 - errors: - unreachable_switch_default: ignore linter: rules: always_declare_return_types: true diff --git a/flutter_quill_extensions/lib/src/editor/image/widgets/image_resizer.dart b/flutter_quill_extensions/lib/src/editor/image/widgets/image_resizer.dart index a8d2609b0..5f481f520 100644 --- a/flutter_quill_extensions/lib/src/editor/image/widgets/image_resizer.dart +++ b/flutter_quill_extensions/lib/src/editor/image/widgets/image_resizer.dart @@ -1,9 +1,7 @@ import 'package:flutter/cupertino.dart' show CupertinoActionSheet, CupertinoActionSheetAction; -import 'package:flutter/foundation.dart' show defaultTargetPlatform; -import 'package:flutter/material.dart' show Slider, Card; +import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' show SchedulerBinding; -import 'package:flutter/widgets.dart'; import 'package:flutter_quill/internal.dart'; class ImageResizer extends StatefulWidget { @@ -39,21 +37,10 @@ class ImageResizerState extends State { @override Widget build(BuildContext context) { - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - return _showCupertinoMenu(); - case TargetPlatform.android: - return _showMaterialMenu(); - case TargetPlatform.macOS: - case TargetPlatform.windows: - case TargetPlatform.linux: - case TargetPlatform.fuchsia: - return _showMaterialMenu(); - default: - throw UnsupportedError( - 'Not supposed to be invoked for $defaultTargetPlatform', - ); + if (Theme.of(context).isCupertino) { + return _showCupertinoMenu(); } + return _showMaterialMenu(); } Widget _showMaterialMenu() { @@ -88,7 +75,7 @@ class ImageResizerState extends State { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Card( - child: Slider( + child: Slider.adaptive( value: isWidth ? _width : _height, max: isWidth ? widget.maxWidth : widget.maxHeight, divisions: 1000, diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index b3c49ddc0..e5e7888eb 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -323,11 +323,6 @@ class QuillRawEditorState extends EditorState case TargetPlatform.windows: widget.config.focusNode.unfocus(); break; - default: - throw UnsupportedError( - 'The platform ${defaultTargetPlatform.name} is not supported in the' - ' _defaultOnTapOutside()', - ); } } diff --git a/lib/src/editor/widgets/text/text_selection.dart b/lib/src/editor/widgets/text/text_selection.dart index fdc66bcd4..66fee30cd 100644 --- a/lib/src/editor/widgets/text/text_selection.dart +++ b/lib/src/editor/widgets/text/text_selection.dart @@ -317,8 +317,6 @@ class EditorTextSelectionOverlay { ? newSelection.extent : const TextPosition(offset: 0); break; - default: - throw ArgumentError('Invalid position'); } final currSelection = newSelection != null @@ -456,8 +454,6 @@ class _TextSelectionHandleOverlay extends StatefulWidget { return renderObject.selectionStartInViewport; case _TextSelectionHandlePosition.end: return renderObject.selectionEndInViewport; - default: - throw ArgumentError('Invalid position'); } } } @@ -547,8 +543,6 @@ class _TextSelectionHandleOverlayState isNormalized ? position.offset : widget.selection.extentOffset, ); break; - default: - throw ArgumentError('Invalid widget.position'); } if (newSelection.baseOffset >= newSelection.extentOffset) { From 2565ae4e69c3db69a0a8bafe91d3b6a1f32fba22 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 16:59:39 +0300 Subject: [PATCH 08/11] chore(release): prepare to publish 11.0.0-dev.16 --- CHANGELOG.md | 5 ++++- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 299c7f937..1f1c44468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.0.0-dev.16] - 2024-12-13 + ### Changed - Address warnings of `unreachable_switch_default` (introduced in Dart 3.6). @@ -157,7 +159,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Apple-specific font dependency for subscript and superscript functionality from the example. - **BREAKING**: The [`super_clipboard`](https://pub.dev/packages/super_clipboard) plugin, To restore legacy behavior for `super_clipboard`, use [`flutter_quill_extensions`](https://pub.dev/packages/flutter_quill_extensions) package and `FlutterQuillExtensions.useSuperClipboardPlugin()`. -[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.15...HEAD +[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.16...HEAD +[11.0.0-dev.16]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.15...v11.0.0-dev.16 [11.0.0-dev.15]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.14...v11.0.0-dev.15 [11.0.0-dev.14]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.13...v11.0.0-dev.14 [11.0.0-dev.13]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0-dev.12...v11.0.0-dev.13 diff --git a/example/pubspec.lock b/example/pubspec.lock index a833c182b..0e4d870a1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -217,7 +217,7 @@ packages: path: ".." relative: true source: path - version: "11.0.0-dev.15" + version: "11.0.0-dev.16" flutter_quill_delta_from_html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 768d838a5..f7fb7b017 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: "A rich text editor built for Android, iOS, Web, and desktop platforms. It's the WYSIWYG editor and a Quill component for Flutter." -version: 11.0.0-dev.15 +version: 11.0.0-dev.16 homepage: https://github.com/singerdmx/flutter-quill/ repository: https://github.com/singerdmx/flutter-quill/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ From 8a6fadad61eedac146766029cae52bf400cce484 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 17:01:15 +0300 Subject: [PATCH 09/11] chore(release): prepare to publish flutter_quill_extensions 11.0.0-dev.7 --- flutter_quill_extensions/CHANGELOG.md | 2 ++ flutter_quill_extensions/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 06c044599..8cfce1354 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.0.0-dev.7] - 2024-12-13 + ### Changed - Address warnings of `unreachable_switch_default` (introduced in Dart 3.6). diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 740e30384..2485be14c 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill to support loading images and videos -version: 11.0.0-dev.6 +version: 11.0.0-dev.7 homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ From 77c75fe078d220ea1fcdf816659089f304a56006 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 17:02:25 +0300 Subject: [PATCH 10/11] chore(example): update pubspec.lock --- example/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 0e4d870a1..4c13c0714 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -232,7 +232,7 @@ packages: path: "../flutter_quill_extensions" relative: true source: path - version: "11.0.0-dev.6" + version: "11.0.0-dev.7" flutter_quill_test: dependency: "direct dev" description: From d745801a3ed044fc360510adde739c0f9811a319 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 13 Dec 2024 17:06:50 +0300 Subject: [PATCH 11/11] chore(example): update iOS example app for SPM by flutter build ios --config-only --- example/ios/Runner.xcodeproj/project.pbxproj | 36 ++++++-------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index ac474400d..703875deb 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -12,11 +12,11 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 6AB2329557D1981D6B17B057 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 04DE71649CFF61B6EA443C6C /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 95A4F67B5B7C7331534DAE52 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7CC42284313298068A87F511 /* Pods_RunnerTests.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -190,9 +190,6 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -203,13 +200,15 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - DEF0130752E58C6B1DAAA7EE /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -218,9 +217,6 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -246,6 +242,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -353,23 +352,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - DEF0130752E58C6B1DAAA7EE /* [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; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -734,12 +716,14 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency;