diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..6cee88e --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,55 @@ +name: โœ… Code Quality Checks + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + checks: + name: ๐Ÿ” Code Quality and Formatting Verification + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: ๐Ÿ“ฅ Checkout Repository + uses: actions/checkout@v4 + + - name: ๐Ÿ› ๏ธ Setup Flutter (Stable) + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + cache: true + + - name: ๐Ÿ“ฆ Install All Package Dependencies + run: dart ./scripts/pub_get.dart + + - name: ๐Ÿ“ฆ Install Example Project Dependencies + run: flutter pub get -C quill_native_bridge/example + + - name: ๐ŸŽจ Perform Flutter Analysis + run: flutter analyze --write=flutter_analyze.log + + - if: ${{ !cancelled() }} + uses: yorifuji/flutter-analyze-commenter@v1 + with: + analyze-log: flutter_analyze.log + verbose: false + + - name: ๐Ÿ”Ž Validate Dart Code Formatting + run: dart format --set-exit-if-changed . + + - name: ๐Ÿ”„ Preview Potential Dart Fixes + run: dart fix --dry-run + + - name: ๐Ÿ“ฆ Verify Package Readiness for Publishing + run: dart ./scripts/publish_dry_run.dart + + # Ktlint: https://github.com/pinterest/ktlint + - name: ๐ŸŽจ Verify Kotlin Code Style with Ktlint + uses: ScaCap/action-ktlint@master + with: + github_token: ${{ secrets.github_token }} + reporter: github-pr-review + fail_on_error: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index bc2bdb1..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: ๐Ÿงช Run Tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - tests: - name: Check lints and tests - runs-on: ubuntu-latest - - steps: - - name: ๐Ÿ“ฆ Checkout repository - uses: actions/checkout@v4 - - - name: ๐Ÿ› ๏ธ Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - cache: true - - - name: ๐Ÿ” Verify Flutter installation - run: flutter --version - - # TODO: Might use https://pub.dev/packages/melos and move the packages (e.g., `quill_native_brdige`) into the 'packages' directory - - - name: ๐Ÿ“ฆ Install quill_native_bridge dependencies - run: flutter pub get -C quill_native_bridge - - - name: ๐Ÿ“ฆ Install quill_native_bridge_android dependencies - run: flutter pub get -C quill_native_bridge_android - - - name: ๐Ÿ“ฆ Install quill_native_bridge_ios dependencies - run: flutter pub get -C quill_native_bridge_ios - - - name: ๐Ÿ“ฆ Install quill_native_bridge_linux dependencies - run: flutter pub get -C quill_native_bridge_linux - - - name: ๐Ÿ“ฆ Install quill_native_bridge_macos dependencies - run: flutter pub get -C quill_native_bridge_macos - - - name: ๐Ÿ“ฆ Install quill_native_bridge_platform_interface dependencies - run: flutter pub get -C quill_native_bridge_platform_interface - - - name: ๐Ÿ“ฆ Install quill_native_bridge_web dependencies - run: flutter pub get -C quill_native_bridge_web - - - name: ๐Ÿ“ฆ Install quill_native_bridge_windows dependencies - run: flutter pub get -C quill_native_bridge_windows - - - name: ๐Ÿ“ฆ Install the example's dependencies - run: flutter pub get -C quill_native_bridge/example --enforce-lockfile - - - name: ๐Ÿ” Run Flutter analysis - run: flutter analyze - - - name: ๐Ÿงน Check Dart code formatting - run: dart format --set-exit-if-changed . - - - name: ๐Ÿ” Preview Dart proposed changes - run: dart fix --dry-run - - # TODO: Run the check (flutter pub publish --dry-run) for all packages (e.g., quill_native_bridge_platform_interface) - - - name: ๐Ÿงช Run Flutter tests - run: | - for package in quill_native_bridge quill_native_bridge_platform_interface quill_native_bridge_android quill_native_bridge_ios quill_native_bridge_macos quill_native_bridge_windows quill_native_bridge_linux quill_native_bridge_web; do - if [ -d "$package/test" ]; then - (cd "$package" && flutter test) - else - echo "No test directory found for $package. Skipping." - fi - done - - # TODO: Run integration tests in CI in a seperate workflow for tests with support for all platforms - # # From https://docs.flutter.dev/get-started/install/linux/desktop#development-tools - # - name: ๐Ÿง Install required Linux packages to run integration tests - # run: sudo apt-get update -y && sudo apt-get upgrade -y && sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa && sudo apt-get install clang cmake git ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev - - # # See https://docs.flutter.dev/get-started/install/linux/desktop#development-tools - # - name: ๐Ÿง Checking the package versions compatibility with Flutter - # run: flutter doctor - - # - name: ๐Ÿงช Run Flutter integration tests - # run: cd quill_native_bridge/example && flutter test integration_test -d linux - # timeout-minutes: 5 - diff --git a/.github/workflows/swift-format-check.yml b/.github/workflows/swift-format-check.yml new file mode 100644 index 0000000..4a76333 --- /dev/null +++ b/.github/workflows/swift-format-check.yml @@ -0,0 +1,52 @@ +name: โœ… Swift Formatting Check + +# Separated from checks.yml since it takes longer to build swift-format from source +on: + push: + branches: [main] + paths: + - quill_native_bridge_ios/ios/quill_native_bridge_ios/**/*.swift + - quill_native_bridge_macos/macos/quill_native_bridge_macos/**/*.swift + - .github/workflows/swift-format-check.yml + pull_request: + paths: + - quill_native_bridge_ios/ios/quill_native_bridge_ios/**/*.swift + - quill_native_bridge_macos/macos/quill_native_bridge_macos/**/*.swift + - .github/workflows/swift-format-check.yml + +jobs: + format-check: + runs-on: ubuntu-latest + timeout-minutes: 10 + + env: + SWIFT_FORMAT_VERSION: 600.0.0 + + steps: + - name: ๐Ÿ“ฅ Checkout Repository + uses: actions/checkout@v4 + + - name: ๐Ÿ› ๏ธ Setup Swift + uses: swift-actions/setup-swift@v2 + + - name: ๐Ÿ“ฆ Install Swift Format + run: | + git clone --branch $SWIFT_FORMAT_VERSION --depth 1 https://github.com/swiftlang/swift-format.git + cd swift-format + swift build -c release + sudo mv .build/release/swift-format /usr/local/bin/ + + - name: ๐Ÿ” Verify Swift Format installation + run: swift-format --version + + - name: โ„น๏ธ Print the default configuration + run: swift-format dump-configuration + + - name: ๐Ÿ”Ž Validate Swift Code Formatting + run: | + swift-format lint -r quill_native_bridge_ios/ios/quill_native_bridge_ios --strict + swift-format lint -r quill_native_bridge_macos/macos/quill_native_bridge_macos --strict + + # SwiftLint: https://github.com/realm/SwiftLint + # - name: ๐ŸŽจ Verify Swift Code Style with SwiftLint + # uses: norio-nomura/action-swiftlint@3.2.1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2dfc4d6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: ๐Ÿงช Run Tests + +on: + pull_request: + branches: [main] + +jobs: + desktop-tests: + name: ๐Ÿ–ฅ๏ธ Desktop Tests (${{ matrix.os }} Latest) + strategy: + # This prevents one failure from stopping the entire run. + fail-fast: false + # TODO: Restore windows, run Android and iOS unit tests, cache swift-format + matrix: + os: [ubuntu, macos] + runs-on: ${{ matrix.os }}-latest + timeout-minutes: 30 + + steps: + - name: ๐Ÿ“ฅ Checkout Repository + uses: actions/checkout@v4 + + - name: ๐Ÿ› ๏ธ Setup Flutter (Stable) + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + cache: true + + - name: ๐Ÿ“ฆ Install all package dependencies + run: dart ./scripts/pub_get.dart + + - name: ๐Ÿ“ฆ Install example dependencies + run: flutter pub get -C quill_native_bridge/example + + - name: ๐Ÿงฉ Run Flutter unit tests + run: dart ./scripts/test.dart + timeout-minutes: 5 + + - name: ๐Ÿ“ฆ Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update -y + sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev + + # For more details: https://docs.flutter.dev/testing/integration-tests#test-on-a-desktop-platform + + - name: ๐Ÿงช Run Flutter integration tests on Linux + if: runner.os == 'Linux' + uses: smithki/xvfb-action@v1.1.2 + with: + run: flutter test integration_test -d linux -r github + working-directory: quill_native_bridge/example + + - name: ๐Ÿงช Run Flutter integration tests on Non-Linux platforms + if: runner.os != 'Linux' + run: flutter test integration_test -d ${{ runner.os }} + working-directory: quill_native_bridge/example diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..ddc32b1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,26 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + # Ignore generated files + - '**/*.g.dart' + - '**/*.mocks.dart' # Mockito @GenerateMocks (https://pub.dev/packages/mockito) + +linter: + rules: + - always_declare_return_types + - avoid_escaping_inner_quotes + - avoid_print + - avoid_void_async + - directives_ordering + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_relative_imports + - prefer_single_quotes + - unnecessary_parenthesis + - avoid_web_libraries_in_flutter + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - avoid_slow_async_io diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..176809f --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,10 @@ +name: quill_native_bridge_workspace +publish_to: 'none' + +environment: + sdk: '>=3.0.0 <4.0.0' + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 diff --git a/quill_native_bridge/analysis_options.yaml b/quill_native_bridge/analysis_options.yaml deleted file mode 100644 index 3204d52..0000000 --- a/quill_native_bridge/analysis_options.yaml +++ /dev/null @@ -1,31 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -linter: - rules: - always_declare_return_types: true - always_put_required_named_parameters_first: true - annotate_overrides: true - avoid_empty_else: true - avoid_escaping_inner_quotes: true - avoid_print: true - avoid_types_on_closure_parameters: true - avoid_void_async: true - directives_ordering: true - omit_local_variable_types: true - prefer_const_constructors: true - prefer_const_constructors_in_immutables: true - prefer_const_declarations: true - prefer_final_fields: true - prefer_final_in_for_each: true - prefer_final_locals: true - prefer_initializing_formals: true - prefer_int_literals: true - prefer_interpolation_to_compose_strings: true - prefer_relative_imports: true - prefer_single_quotes: true - sort_constructors_first: true - sort_unnamed_constructors_first: true - unnecessary_lambdas: true - unnecessary_parenthesis: true - unnecessary_string_interpolations: true - avoid_web_libraries_in_flutter: true diff --git a/quill_native_bridge/example/analysis_options.yaml b/quill_native_bridge/example/analysis_options.yaml index 799c9a5..f9b3034 100644 --- a/quill_native_bridge/example/analysis_options.yaml +++ b/quill_native_bridge/example/analysis_options.yaml @@ -1,30 +1 @@ include: package:flutter_lints/flutter.yaml - -linter: - rules: - always_declare_return_types: true - always_put_required_named_parameters_first: true - annotate_overrides: true - avoid_empty_else: true - avoid_escaping_inner_quotes: true - avoid_print: true - avoid_types_on_closure_parameters: true - avoid_void_async: true - directives_ordering: true - omit_local_variable_types: true - prefer_const_constructors: true - prefer_const_constructors_in_immutables: true - prefer_const_declarations: true - prefer_final_fields: true - prefer_final_in_for_each: true - prefer_final_locals: true - prefer_initializing_formals: true - prefer_int_literals: true - prefer_interpolation_to_compose_strings: true - prefer_relative_imports: true - prefer_single_quotes: true - sort_constructors_first: true - sort_unnamed_constructors_first: true - unnecessary_lambdas: true - unnecessary_parenthesis: true - unnecessary_string_interpolations: true diff --git a/quill_native_bridge/example/android/app/src/main/kotlin/dev/flutterquill/quill_native_bridge_example/MainActivity.kt b/quill_native_bridge/example/android/app/src/main/kotlin/dev/flutterquill/quill_native_bridge_example/MainActivity.kt index a36c871..94e421d 100644 --- a/quill_native_bridge/example/android/app/src/main/kotlin/dev/flutterquill/quill_native_bridge_example/MainActivity.kt +++ b/quill_native_bridge/example/android/app/src/main/kotlin/dev/flutterquill/quill_native_bridge_example/MainActivity.kt @@ -2,4 +2,4 @@ package dev.flutterquill.quill_native_bridge_example import io.flutter.embedding.android.FlutterActivity -class MainActivity: FlutterActivity() +class MainActivity : FlutterActivity() diff --git a/quill_native_bridge/example/integration_test/quill_native_bridge_test.dart b/quill_native_bridge/example/integration_test/quill_native_bridge_test.dart index cc8fc95..2d206dd 100644 --- a/quill_native_bridge/example/integration_test/quill_native_bridge_test.dart +++ b/quill_native_bridge/example/integration_test/quill_native_bridge_test.dart @@ -11,6 +11,14 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // TODO: Write tests for copying other image formats (jpeg, webp, png etc...) + // TODO: Improve the integration tests + + // TODO: Fix the integration tests failing on Windows: https://github.com/FlutterQuill/quill-native-bridge/actions/runs/12323723239/job/34399916127?pr=10 + if (defaultTargetPlatform == TargetPlatform.windows) { + test('no op', () {}); + return; + } + group('getClipboardImage and copyImageToClipboard', () { test('copying images to the clipboard should make them accessible', () async { diff --git a/quill_native_bridge/example/pubspec.lock b/quill_native_bridge/example/pubspec.lock index f1d5d3a..9770fee 100644 --- a/quill_native_bridge/example/pubspec.lock +++ b/quill_native_bridge/example/pubspec.lock @@ -352,10 +352,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: @@ -520,10 +520,10 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: @@ -545,7 +545,7 @@ packages: path: "../../quill_native_bridge_android" relative: true source: path - version: "0.0.1" + version: "0.0.1+2" quill_native_bridge_ios: dependency: "direct overridden" description: @@ -573,7 +573,7 @@ packages: path: "../../quill_native_bridge_platform_interface" relative: true source: path - version: "0.0.1" + version: "0.0.1+1" quill_native_bridge_web: dependency: "direct overridden" description: @@ -685,10 +685,10 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: @@ -786,5 +786,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.6.0-0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/quill_native_bridge/pubspec.yaml b/quill_native_bridge/pubspec.yaml index 48f4d1e..b1627f0 100644 --- a/quill_native_bridge/pubspec.yaml +++ b/quill_native_bridge/pubspec.yaml @@ -25,7 +25,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 mockito: ^5.4.4 build_runner: ^2.4.13 plugin_platform_interface: ^2.1.8 diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgeImpl.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgeImpl.kt index 3fa326a..9e5b63a 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgeImpl.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgeImpl.kt @@ -10,27 +10,29 @@ import dev.flutterquill.quill_native_bridge.generated.QuillNativeBridgeApi import dev.flutterquill.quill_native_bridge.saveImage.SaveImageHandler import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -class QuillNativeBridgeImpl(private val context: Context) : QuillNativeBridgeApi { +class QuillNativeBridgeImpl( + private val context: Context, +) : QuillNativeBridgeApi { private var activityPluginBinding: ActivityPluginBinding? = null override fun getClipboardHtml(): String? = ClipboardRichTextHandler.getClipboardHtml(context) - override fun copyHtmlToClipboard(html: String) = - ClipboardRichTextHandler.copyHtmlToClipboard(context, html) + override fun copyHtmlToClipboard(html: String) = ClipboardRichTextHandler.copyHtmlToClipboard(context, html) - override fun getClipboardImage(): ByteArray? = ClipboardReadImageHandler.getClipboardImage( - context, - // Will convert the image to PNG - imageType = ClipboardReadImageHandler.ImageType.AnyExceptGif, - ) + override fun getClipboardImage(): ByteArray? = + ClipboardReadImageHandler.getClipboardImage( + context, + // Will convert the image to PNG + imageType = ClipboardReadImageHandler.ImageType.AnyExceptGif, + ) - override fun copyImageToClipboard(imageBytes: ByteArray) = - ClipboardWriteImageHandler.copyImageToClipboard(context, imageBytes) + override fun copyImageToClipboard(imageBytes: ByteArray) = ClipboardWriteImageHandler.copyImageToClipboard(context, imageBytes) - override fun getClipboardGif(): ByteArray? = ClipboardReadImageHandler.getClipboardImage( - context, - imageType = ClipboardReadImageHandler.ImageType.Gif, - ) + override fun getClipboardGif(): ByteArray? = + ClipboardReadImageHandler.getClipboardImage( + context, + imageType = ClipboardReadImageHandler.ImageType.Gif, + ) override fun openGalleryApp() { // TODO(save-image): Test on Android marshmallow (API 23) @@ -47,7 +49,7 @@ class QuillNativeBridgeImpl(private val context: Context) : QuillNativeBridgeApi fileExtension: String, mimeType: String, albumName: String?, - callback: (Result) -> Unit + callback: (Result) -> Unit, ) = SaveImageHandler.saveImageToGallery( context, getActivityPluginBindingOrThrow(), @@ -56,15 +58,16 @@ class QuillNativeBridgeImpl(private val context: Context) : QuillNativeBridgeApi fileExtension = fileExtension, mimeType = mimeType, albumName = albumName, - callback = callback + callback = callback, ) - private fun getActivityPluginBindingOrThrow(): ActivityPluginBinding { - return activityPluginBinding - ?: throw IllegalStateException("The Flutter activity binding was not set. This indicates a bug in `${QuillNativeBridgePlugin::class.simpleName}`.") - } + private fun getActivityPluginBindingOrThrow(): ActivityPluginBinding = + activityPluginBinding + ?: throw IllegalStateException( + "The Flutter activity binding was not set. This indicates a bug in `${QuillNativeBridgePlugin::class.simpleName}`.", + ) fun setActivityPluginBinding(activityPluginBinding: ActivityPluginBinding?) { this.activityPluginBinding = activityPluginBinding } -} \ No newline at end of file +} diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgePlugin.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgePlugin.kt index 16bd835..72d304b 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgePlugin.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgePlugin.kt @@ -7,8 +7,9 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -class QuillNativeBridgePlugin : FlutterPlugin, ActivityAware { - +class QuillNativeBridgePlugin : + FlutterPlugin, + ActivityAware { companion object { const val TAG = "QuillNativeBridgePlugin" } @@ -55,12 +56,15 @@ class QuillNativeBridgePlugin : FlutterPlugin, ActivityAware { Log.wtf( TAG, "The `${::pluginApi.name}` is not initialized. Failed to update Flutter activity binding " + - "reference for `${QuillNativeBridgeImpl::class.simpleName}` in `$methodName`." + "reference for `${QuillNativeBridgeImpl::class.simpleName}` in `$methodName`.", ) } @VisibleForTesting - internal fun setActivityPluginBinding(binding: ActivityPluginBinding, methodName: String) { + internal fun setActivityPluginBinding( + binding: ActivityPluginBinding, + methodName: String, + ) { activityPluginBinding = binding pluginApi?.setActivityPluginBinding(binding) ?: logApiNotSetError(methodName) } diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardReadImageHandler.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardReadImageHandler.kt index e55f9f0..b656029 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardReadImageHandler.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardReadImageHandler.kt @@ -66,14 +66,16 @@ object ClipboardReadImageHandler { val clipboardItem = clipData.getItemAt(0) val imageUri = clipboardItem.uri - val matchMimeType: Boolean = when (imageType) { - ImageType.Png -> clipData.description.hasMimeType(MIME_TYPE_IMAGE_PNG) - ImageType.Jpeg -> clipData.description.hasMimeType(MIME_TYPE_IMAGE_JPEG) - ImageType.AnyExceptGif -> clipData.description.hasMimeType(MIME_TYPE_IMAGE_ALL) && - !clipData.description.hasMimeType(MIME_TYPE_IMAGE_GIF) - - ImageType.Gif -> clipData.description.hasMimeType(MIME_TYPE_IMAGE_GIF) - } + val matchMimeType: Boolean = + when (imageType) { + ImageType.Png -> clipData.description.hasMimeType(MIME_TYPE_IMAGE_PNG) + ImageType.Jpeg -> clipData.description.hasMimeType(MIME_TYPE_IMAGE_JPEG) + ImageType.AnyExceptGif -> + clipData.description.hasMimeType(MIME_TYPE_IMAGE_ALL) && + !clipData.description.hasMimeType(MIME_TYPE_IMAGE_GIF) + + ImageType.Gif -> clipData.description.hasMimeType(MIME_TYPE_IMAGE_GIF) + } if (imageUri == null || !matchMimeType) { // Image URI is null or the mime type doesn't match. // This is not widely supported but some apps do store images as file paths in a text @@ -105,13 +107,12 @@ object ClipboardReadImageHandler { * @throws FileNotFoundException Could be thrown when the [Uri] is no longer on the clipboard. * */ @Throws(Exception::class) - private fun Uri.readOrThrow( - context: Context, - ) = try { - context.contentResolver.openInputStream(this)?.close() - } catch (e: Exception) { - throw e - } + private fun Uri.readOrThrow(context: Context) = + try { + context.contentResolver.openInputStream(this)?.close() + } catch (e: Exception) { + throw e + } /** * Get the clipboard Image. @@ -122,10 +123,11 @@ object ClipboardReadImageHandler { ): ByteArray? { val primaryClipData = getPrimaryClip(context) ?: return null - val imageUri = getImageUri( - clipData = primaryClipData, - imageType = imageType, - ) ?: return null + val imageUri = + getImageUri( + clipData = primaryClipData, + imageType = imageType, + ) ?: return null try { imageUri.readOrThrow(context) @@ -134,30 +136,32 @@ object ClipboardReadImageHandler { is SecurityException -> throw FlutterError( "FILE_READ_PERMISSION_DENIED", "An image exists on the clipboard, but the app no longer " + - "has permission to access it. This may be due to the app's " + - "lifecycle or a recent app restart: ${e.message}", + "has permission to access it. This may be due to the app's " + + "lifecycle or a recent app restart: ${e.message}", e.toString(), ) is FileNotFoundException -> throw FlutterError( "FILE_NOT_FOUND", "The image file can't be found, the provided URI could not be opened: ${e.message}", - e.toString() + e.toString(), ) else -> throw FlutterError( "UNKNOWN_ERROR_READING_FILE", "An unknown occurred while reading the image file URI: ${e.message}", - e.toString() + e.toString(), ) } } - val imageBytes = when (imageType) { - ImageType.Png, ImageType.Jpeg, - ImageType.AnyExceptGif -> getClipboardImageAsPng(context, imageUri) + val imageBytes = + when (imageType) { + ImageType.Png, ImageType.Jpeg, + ImageType.AnyExceptGif, + -> getClipboardImageAsPng(context, imageUri) - ImageType.Gif -> getClipboardGif(context, imageUri) - } + ImageType.Gif -> getClipboardGif(context, imageUri) + } return imageBytes } @@ -167,43 +171,45 @@ object ClipboardReadImageHandler { * */ private fun getClipboardImageAsPng( context: Context, - imageUri: Uri + imageUri: Uri, ): ByteArray { - val bitmap: Bitmap = try { - ImageDecoderCompat.decodeBitmapFromUri(context.contentResolver, imageUri) - } catch (e: IOException) { - throw FlutterError( - "COULD_NOT_DECODE_IMAGE", - "Could not decode bitmap from Uri: ${e.message}", - e.toString(), - ) - } - - val imageBytes = ByteArrayOutputStream().use { outputStream -> - val compressedSuccessfully = - bitmap.compress( - Bitmap.CompressFormat.PNG, - /** - * Quality will be ignored for png images. See [Bitmap.CompressFormat.PNG] docs - * */ - 100, - outputStream - ) - if (!compressedSuccessfully) { + val bitmap: Bitmap = + try { + ImageDecoderCompat.decodeBitmapFromUri(context.contentResolver, imageUri) + } catch (e: IOException) { throw FlutterError( - "COULD_NOT_COMPRESS_IMAGE", - "Unknown error while compressing the image", - null, + "COULD_NOT_DECODE_IMAGE", + "Could not decode bitmap from Uri: ${e.message}", + e.toString(), ) } - outputStream.toByteArray() - } + + val imageBytes = + ByteArrayOutputStream().use { outputStream -> + val compressedSuccessfully = + bitmap.compress( + Bitmap.CompressFormat.PNG, + /** + * Quality will be ignored for png images. See [Bitmap.CompressFormat.PNG] docs + * */ + 100, + outputStream, + ) + if (!compressedSuccessfully) { + throw FlutterError( + "COULD_NOT_COMPRESS_IMAGE", + "Unknown error while compressing the image", + null, + ) + } + outputStream.toByteArray() + } return imageBytes } private fun getClipboardGif( context: Context, - imageUri: Uri + imageUri: Uri, ): ByteArray { try { val imageBytes = uriToByteArray(context, imageUri) @@ -217,11 +223,13 @@ object ClipboardReadImageHandler { } } - private fun uriToByteArray(context: Context, uri: Uri): ByteArray { - return checkNotNull(context.contentResolver.openInputStream(uri)) { + private fun uriToByteArray( + context: Context, + uri: Uri, + ): ByteArray = + checkNotNull(context.contentResolver.openInputStream(uri)) { "Input stream is null, the provider might have recently crashed." }.use { inputStream -> inputStream.readBytes() } - } -} \ No newline at end of file +} diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardRichTextHandler.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardRichTextHandler.kt index a7034ce..2b06bbc 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardRichTextHandler.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardRichTextHandler.kt @@ -27,10 +27,11 @@ object ClipboardRichTextHandler { val clipboardItem = primaryClipData.getItemAt(0) - val htmlText = clipboardItem.htmlText ?: throw FlutterError( - "HTML_TEXT_NULL", - "Expected the HTML Text from the Clipboard to be not null" - ) + val htmlText = + clipboardItem.htmlText ?: throw FlutterError( + "HTML_TEXT_NULL", + "Expected the HTML Text from the Clipboard to be not null", + ) return htmlText } @@ -51,4 +52,4 @@ object ClipboardRichTextHandler { ) } } -} \ No newline at end of file +} diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardWriteImageHandler.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardWriteImageHandler.kt index e6cb5ae..136260c 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardWriteImageHandler.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/clipboard/ClipboardWriteImageHandler.kt @@ -15,15 +15,16 @@ object ClipboardWriteImageHandler { context: Context, imageBytes: ByteArray, ) { - val bitmap: Bitmap = try { - ImageDecoderCompat.decodeBitmapFromBytes(imageBytes) - } catch (e: IOException) { - throw FlutterError( - "INVALID_IMAGE", - "The provided image bytes are invalid, image could not be decoded: ${e.message}", - e.toString(), - ) - } + val bitmap: Bitmap = + try { + ImageDecoderCompat.decodeBitmapFromBytes(imageBytes) + } catch (e: IOException) { + throw FlutterError( + "INVALID_IMAGE", + "The provided image bytes are invalid, image could not be decoded: ${e.message}", + e.toString(), + ) + } val tempImageFile = File(context.cacheDir, "temp_clipboard_image.png") @@ -57,21 +58,22 @@ object ClipboardWriteImageHandler { val authority = "${context.packageName}.fileprovider" - val imageUri = try { - FileProvider.getUriForFile( - context, - authority, - tempImageFile, - ) - } catch (e: IllegalArgumentException) { - throw FlutterError( - "ANDROID_MANIFEST_NOT_CONFIGURED", - "You need to configure your AndroidManifest.xml file " + + val imageUri = + try { + FileProvider.getUriForFile( + context, + authority, + tempImageFile, + ) + } catch (e: IllegalArgumentException) { + throw FlutterError( + "ANDROID_MANIFEST_NOT_CONFIGURED", + "You need to configure your AndroidManifest.xml file " + "to register the provider with the meta-data with authority " + authority, - e.toString(), - ) - } + e.toString(), + ) + } try { val clipboard = @@ -89,4 +91,4 @@ object ClipboardWriteImageHandler { ) } } -} \ No newline at end of file +} diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/generated/GeneratedMessages.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/generated/GeneratedMessages.kt index f80bddd..21cd322 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/generated/GeneratedMessages.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/generated/GeneratedMessages.kt @@ -12,25 +12,22 @@ import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer -private fun wrapResult(result: Any?): List { - return listOf(result) -} +private fun wrapResult(result: Any?): List = listOf(result) -private fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } -} +private fun wrapError(exception: Throwable): List = + if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details, + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception), + ) + } /** * Error class for passing custom error details to Flutter via a thrown PlatformException. @@ -38,161 +35,231 @@ private fun wrapError(exception: Throwable): List { * @property message The error message. * @property details The error details. Must be a datatype supported by the api codec. */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null +class FlutterError( + val code: String, + override val message: String? = null, + val details: Any? = null, ) : Throwable() + private open class GeneratedMessagesPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) - } -} + override fun readValueOfType( + type: Byte, + buffer: ByteBuffer, + ): Any? = super.readValueOfType(type, buffer) + override fun writeValue( + stream: ByteArrayOutputStream, + value: Any?, + ) { + super.writeValue(stream, value) + } +} /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface QuillNativeBridgeApi { - fun getClipboardHtml(): String? - fun copyHtmlToClipboard(html: String) - fun getClipboardImage(): ByteArray? - fun copyImageToClipboard(imageBytes: ByteArray) - fun getClipboardGif(): ByteArray? - fun openGalleryApp() - /** The [fileExtension] is only required for Android APIs before 29. */ - fun saveImageToGallery(imageBytes: ByteArray, name: String, fileExtension: String, mimeType: String, albumName: String?, callback: (Result) -> Unit) - - companion object { - /** The codec used by QuillNativeBridgeApi. */ - val codec: MessageCodec by lazy { - GeneratedMessagesPigeonCodec() - } - /** Sets up an instance of `QuillNativeBridgeApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: QuillNativeBridgeApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.getClipboardHtml$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getClipboardHtml()) - } catch (exception: Throwable) { - wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) + fun getClipboardHtml(): String? + + fun copyHtmlToClipboard(html: String) + + fun getClipboardImage(): ByteArray? + + fun copyImageToClipboard(imageBytes: ByteArray) + + fun getClipboardGif(): ByteArray? + + fun openGalleryApp() + + /** The [fileExtension] is only required for Android APIs before 29. */ + fun saveImageToGallery( + imageBytes: ByteArray, + name: String, + fileExtension: String, + mimeType: String, + albumName: String?, + callback: (Result) -> Unit, + ) + + companion object { + /** The codec used by QuillNativeBridgeApi. */ + val codec: MessageCodec by lazy { + GeneratedMessagesPigeonCodec() } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.copyHtmlToClipboard$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val htmlArg = args[0] as String - val wrapped: List = try { - api.copyHtmlToClipboard(htmlArg) - listOf(null) - } catch (exception: Throwable) { - wrapError(exception) + + /** Sets up an instance of `QuillNativeBridgeApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp( + binaryMessenger: BinaryMessenger, + api: QuillNativeBridgeApi?, + messageChannelSuffix: String = "", + ) { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.getClipboardHtml$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.getClipboardHtml()) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.getClipboardImage$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getClipboardImage()) - } catch (exception: Throwable) { - wrapError(exception) + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.copyHtmlToClipboard$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val htmlArg = args[0] as String + val wrapped: List = + try { + api.copyHtmlToClipboard(htmlArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.copyImageToClipboard$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val imageBytesArg = args[0] as ByteArray - val wrapped: List = try { - api.copyImageToClipboard(imageBytesArg) - listOf(null) - } catch (exception: Throwable) { - wrapError(exception) + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.getClipboardImage$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.getClipboardImage()) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.getClipboardGif$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getClipboardGif()) - } catch (exception: Throwable) { - wrapError(exception) + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.copyImageToClipboard$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val imageBytesArg = args[0] as ByteArray + val wrapped: List = + try { + api.copyImageToClipboard(imageBytesArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.openGalleryApp$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.openGalleryApp() - listOf(null) - } catch (exception: Throwable) { - wrapError(exception) + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.getClipboardGif$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.getClipboardGif()) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.saveImageToGallery$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val imageBytesArg = args[0] as ByteArray - val nameArg = args[1] as String - val fileExtensionArg = args[2] as String - val mimeTypeArg = args[3] as String - val albumNameArg = args[4] as String? - api.saveImageToGallery(imageBytesArg, nameArg, fileExtensionArg, mimeTypeArg, albumNameArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(wrapError(error)) - } else { - reply.reply(wrapResult(null)) - } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.openGalleryApp$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + api.openGalleryApp() + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.quill_native_bridge_android.QuillNativeBridgeApi.saveImageToGallery$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val imageBytesArg = args[0] as ByteArray + val nameArg = args[1] as String + val fileExtensionArg = args[2] as String + val mimeTypeArg = args[3] as String + val albumNameArg = args[4] as String? + api.saveImageToGallery( + imageBytesArg, + nameArg, + fileExtensionArg, + mimeTypeArg, + albumNameArg, + ) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } } - } - } else { - channel.setMessageHandler(null) } - } } - } } diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/saveImage/SaveImageHandler.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/saveImage/SaveImageHandler.kt index e05f3e3..2950feb 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/saveImage/SaveImageHandler.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/saveImage/SaveImageHandler.kt @@ -24,7 +24,6 @@ import java.nio.file.Paths import java.util.UUID object SaveImageHandler { - /** * @return `true` if running on API 29 or newer version. * */ @@ -38,10 +37,11 @@ object SaveImageHandler { private fun isWriteExternalStoragePermissionDeclared(context: Context): Boolean { return try { - val packageInfo = context.packageManager.getPackageInfo( - context.packageName, - PackageManager.GET_PERMISSIONS - ) + val packageInfo = + context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_PERMISSIONS, + ) val permissions = packageInfo.requestedPermissions if (permissions.isNullOrEmpty()) { @@ -65,14 +65,15 @@ object SaveImageHandler { mimeType: String, context: Context, ) { - val imageSaveDirectory = if (albumName != null) { - File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), - albumName - ) - } else { - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - } + val imageSaveDirectory = + if (albumName != null) { + File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + albumName, + ) + } else { + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + } if (!imageSaveDirectory.exists() && !imageSaveDirectory.mkdirs()) { callback.respondFlutterPigeonError( @@ -86,12 +87,12 @@ object SaveImageHandler { val imageFile = File( imageSaveDirectory, - "${name}-${System.currentTimeMillis()}-${UUID.randomUUID()}.${fileExtension}" + "$name-${System.currentTimeMillis()}-${UUID.randomUUID()}.$fileExtension", ) if (imageFile.exists()) { callback.respondFlutterPigeonError( "FILE_EXISTS", - "A file with the name `${imageFile}` already exists.", + "A file with the name `$imageFile` already exists.", null, ) return @@ -104,7 +105,7 @@ object SaveImageHandler { callback.respondFlutterPigeonError( "SAVE_FAILED", "Failed to save the image to the gallery: ${e.message}", - e.toString() + e.toString(), ) return } @@ -128,7 +129,7 @@ object SaveImageHandler { fileExtension: String, mimeType: String, albumName: String?, - callback: (Result) -> Unit + callback: (Result) -> Unit, ) { if (!ImageDecoderCompat.isValidImage(imageBytes)) { callback.respondFlutterPigeonError( @@ -144,69 +145,73 @@ object SaveImageHandler { "ANDROID_MANIFEST_NOT_CONFIGURED", "The uses-permission '${WRITE_EXTERNAL_STORAGE_PERMISSION_NAME}' is not declared in AndroidManifest.xml", "The app is running on Android API ${Build.VERSION.SDK_INT}. Scoped storage" + - " was introduced in ${Build.VERSION_CODES.Q} and is not available on this version.\n" + - "Write to external storage permission is required to save an image to the gallery." + " was introduced in ${Build.VERSION_CODES.Q} and is not available on this version.\n" + + "Write to external storage permission is required to save an image to the gallery.", ) return } // Need to request runtime permission for API 28 and older versions - val hasNecessaryPermission = ContextCompat.checkSelfPermission( - context, - android.Manifest.permission.WRITE_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED + val hasNecessaryPermission = + ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) == PackageManager.PERMISSION_GRANTED if (!hasNecessaryPermission) { ActivityCompat.requestPermissions( activityPluginBinding.activity, arrayOf(WRITE_EXTERNAL_STORAGE_PERMISSION_NAME), - WRITE_TO_EXTERNAL_STORAGE_REQUEST_CODE + WRITE_TO_EXTERNAL_STORAGE_REQUEST_CODE, ) - activityPluginBinding.addRequestPermissionsResultListener(object : - PluginRegistry.RequestPermissionsResultListener { - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ): Boolean { - try { - if (requestCode != WRITE_TO_EXTERNAL_STORAGE_REQUEST_CODE) { - return false - } - val isWriteExternalStoragePermissionRequested = permissions.contentEquals( - arrayOf(WRITE_EXTERNAL_STORAGE_PERMISSION_NAME) - ) - if (!isWriteExternalStoragePermissionRequested) { - Log.w( - QuillNativeBridgePlugin.TAG, - "Unexpected permissions requested. Expected only [$WRITE_EXTERNAL_STORAGE_PERMISSION_NAME], but received: ${permissions.joinToString()}." - ) - } - val isGranted = - grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED - if (!isGranted) { - callback.respondFlutterPigeonError( - "PERMISSION_DENIED", - "Write to external storage permission request has been denied." + activityPluginBinding.addRequestPermissionsResultListener( + object : + PluginRegistry.RequestPermissionsResultListener { + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + ): Boolean { + try { + if (requestCode != WRITE_TO_EXTERNAL_STORAGE_REQUEST_CODE) { + return false + } + val isWriteExternalStoragePermissionRequested = + permissions.contentEquals( + arrayOf(WRITE_EXTERNAL_STORAGE_PERMISSION_NAME), + ) + if (!isWriteExternalStoragePermissionRequested) { + Log.w( + QuillNativeBridgePlugin.TAG, + "Unexpected permissions requested. Expected only [$WRITE_EXTERNAL_STORAGE_PERMISSION_NAME], but received: ${permissions.joinToString()}.", + ) + } + val isGranted = + grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED + if (!isGranted) { + callback.respondFlutterPigeonError( + "PERMISSION_DENIED", + "Write to external storage permission request has been denied.", + ) + return true + } + saveImageToGalleryLegacy( + imageBytes = imageBytes, + name = name, + albumName = albumName, + fileExtension = fileExtension, + callback = callback, + mimeType = mimeType, + context = context, ) return true - } - saveImageToGalleryLegacy( - imageBytes = imageBytes, - name = name, - albumName = albumName, - fileExtension = fileExtension, - callback = callback, - mimeType = mimeType, - context = context, - ) - return true - } finally { - Handler(Looper.getMainLooper()).post { - activityPluginBinding.removeRequestPermissionsResultListener(this) + } finally { + Handler(Looper.getMainLooper()).post { + activityPluginBinding.removeRequestPermissionsResultListener(this) + } } } - } - }) + }, + ) return } @@ -226,18 +231,19 @@ object SaveImageHandler { val contentResolver = context.contentResolver - val contentValues = ContentValues().apply { - put(MediaStore.Images.Media.DISPLAY_NAME, name) - put(MediaStore.Images.Media.MIME_TYPE, mimeType) - put(MediaStore.Images.Media.IS_PENDING, 1) + val contentValues = + ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, name) + put(MediaStore.Images.Media.MIME_TYPE, mimeType) + put(MediaStore.Images.Media.IS_PENDING, 1) - albumName?.let { - put( - MediaStore.Images.Media.RELATIVE_PATH, - Paths.get(Environment.DIRECTORY_PICTURES, it).toString() - ) + albumName?.let { + put( + MediaStore.Images.Media.RELATIVE_PATH, + Paths.get(Environment.DIRECTORY_PICTURES, it).toString(), + ) + } } - } val imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) @@ -256,19 +262,20 @@ object SaveImageHandler { if (rowsUpdated == 0) { Log.e( QuillNativeBridgePlugin.TAG, - "Failed to update image state for URI: $imageUri" + "Failed to update image state for URI: $imageUri", ) } } - val outputStream = contentResolver.openOutputStream(imageUri) ?: run { - callback.respondFlutterPigeonError( - "SAVE_FAILED", - "Could not open the output stream. The provider might have recently crashed." - ) - notifyImageUpdate() - return - } + val outputStream = + contentResolver.openOutputStream(imageUri) ?: run { + callback.respondFlutterPigeonError( + "SAVE_FAILED", + "Could not open the output stream. The provider might have recently crashed.", + ) + notifyImageUpdate() + return + } try { outputStream.use { stream -> stream.write(imageBytes) } @@ -278,9 +285,9 @@ object SaveImageHandler { callback.respondFlutterPigeonError( "SAVE_FAILED", "Failed to save the image to the gallery: ${e.message}", - e.toString() + e.toString(), ) notifyImageUpdate() } } -} \ No newline at end of file +} diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/util/ImageDecoderCompat.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/util/ImageDecoderCompat.kt index 6ebdfa4..ce19580 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/util/ImageDecoderCompat.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/util/ImageDecoderCompat.kt @@ -21,8 +21,8 @@ object ImageDecoderCompat { * @see decodeBitmapFromUri * */ @Throws(IOException::class) - fun decodeBitmapFromBytes(imageBytes: ByteArray): Bitmap { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + fun decodeBitmapFromBytes(imageBytes: ByteArray): Bitmap = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // API 31 and above (use a newer API) val source = ImageDecoder.createSource(imageBytes) ImageDecoder.decodeBitmap(source) @@ -31,7 +31,6 @@ object ImageDecoderCompat { BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) ?: throw IOException("Image could not be decoded using the `BitmapFactory.decodeByteArray`.") } - } /** * Uses [ImageDecoder.decodeBitmap] on Android API 28 and newer, fallback to [BitmapFactory.decodeStream] @@ -41,8 +40,11 @@ object ImageDecoderCompat { * @see decodeBitmapFromBytes * */ @Throws(IOException::class) - fun decodeBitmapFromUri(contentResolver: ContentResolver, imageUri: Uri): Bitmap { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + fun decodeBitmapFromUri( + contentResolver: ContentResolver, + imageUri: Uri, + ): Bitmap = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // API 28 and above (use a newer API) val source = ImageDecoder.createSource(contentResolver, imageUri) ImageDecoder.decodeBitmap(source) @@ -51,12 +53,12 @@ object ImageDecoderCompat { checkNotNull(contentResolver.openInputStream(imageUri)) { "Input stream is null, the provider might have recently crashed." }.use { inputStream -> - val bitmap: Bitmap = BitmapFactory.decodeStream(inputStream) - ?: throw IOException("The image could not be decoded using the `BitmapFactory.decodeStream`.") + val bitmap: Bitmap = + BitmapFactory.decodeStream(inputStream) + ?: throw IOException("The image could not be decoded using the `BitmapFactory.decodeStream`.") bitmap } } - } fun isValidImage(imageBytes: ByteArray) = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) != null -} \ No newline at end of file +} diff --git a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/util/PigeonExtension.kt b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/util/PigeonExtension.kt index ba23728..1516702 100644 --- a/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/util/PigeonExtension.kt +++ b/quill_native_bridge_android/android/src/main/kotlin/dev/flutterquill/quill_native_bridge/util/PigeonExtension.kt @@ -7,7 +7,7 @@ import dev.flutterquill.quill_native_bridge.generated.FlutterError fun ((Result) -> Unit).respondFlutterPigeonError( code: String, message: String? = null, - details: Any? = null + details: Any? = null, ) { this( Result.failure( @@ -15,8 +15,8 @@ fun ((Result) -> Unit).respondFlutterPigeonError( code = code, message = message, details = details, - ) - ) + ), + ), ) } diff --git a/quill_native_bridge_android/android/src/test/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgePluginTest.kt b/quill_native_bridge_android/android/src/test/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgePluginTest.kt index 1e508be..55277f9 100644 --- a/quill_native_bridge_android/android/src/test/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgePluginTest.kt +++ b/quill_native_bridge_android/android/src/test/kotlin/dev/flutterquill/quill_native_bridge/QuillNativeBridgePluginTest.kt @@ -17,7 +17,6 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class QuillNativeBridgePluginTest { - private lateinit var mockPluginApi: QuillNativeBridgeImpl private lateinit var mockApplication: Application private lateinit var mockActivity: Activity @@ -34,10 +33,11 @@ class QuillNativeBridgePluginTest { mockApplication = mock() mockActivity = mock() mockBinaryMessenger = mock() - mockFlutterPluginBinding = mock { - on { applicationContext }.thenReturn(mockApplication) - on { binaryMessenger }.thenReturn(mockBinaryMessenger) - } + mockFlutterPluginBinding = + mock { + on { applicationContext }.thenReturn(mockApplication) + on { binaryMessenger }.thenReturn(mockBinaryMessenger) + } mockActivityBinding = mock { on { activity }.thenReturn(mockActivity) } plugin = QuillNativeBridgePlugin() diff --git a/quill_native_bridge_android/lib/quill_native_bridge_android.dart b/quill_native_bridge_android/lib/quill_native_bridge_android.dart index 912f2c6..5193f75 100644 --- a/quill_native_bridge_android/lib/quill_native_bridge_android.dart +++ b/quill_native_bridge_android/lib/quill_native_bridge_android.dart @@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:quill_native_bridge_platform_interface/internal.dart'; import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart'; -import 'package:quill_native_bridge_platform_interface/src/image_mime_utils.dart'; import 'src/messages.g.dart'; diff --git a/quill_native_bridge_android/pubspec.yaml b/quill_native_bridge_android/pubspec.yaml index df33603..f1198aa 100644 --- a/quill_native_bridge_android/pubspec.yaml +++ b/quill_native_bridge_android/pubspec.yaml @@ -14,7 +14,7 @@ environment: dependencies: flutter: sdk: flutter - quill_native_bridge_platform_interface: ^0.0.1 + quill_native_bridge_platform_interface: ^0.0.1+1 dev_dependencies: flutter_test: diff --git a/quill_native_bridge_android/test/quill_native_bridge_android_test.dart b/quill_native_bridge_android/test/quill_native_bridge_android_test.dart index 3f7e3fd..002bbe9 100644 --- a/quill_native_bridge_android/test/quill_native_bridge_android_test.dart +++ b/quill_native_bridge_android/test/quill_native_bridge_android_test.dart @@ -6,10 +6,9 @@ import 'package:quill_native_bridge_android/quill_native_bridge_android.dart'; import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart'; import 'package:quill_native_bridge_platform_interface/src/image_mime_utils.dart'; -import 'test_api.g.dart'; - @GenerateMocks([TestQuillNativeBridgeApi]) import 'quill_native_bridge_android_test.mocks.dart'; +import 'test_api.g.dart'; void main() { // Required when calling TestQuillNativeBridgeApi.setUp() @@ -49,7 +48,7 @@ void main() { verify(mockHostApi.getClipboardHtml()).called(1); expect(nullHtml, isNull); - final exampleHtml = 'An HTML'; + const exampleHtml = 'An HTML'; when(mockHostApi.getClipboardHtml()).thenReturn(exampleHtml); final nonNullHtml = await plugin.getClipboardHtml(); @@ -61,7 +60,7 @@ void main() { test( 'copyHtmlToClipboard delegates to _hostApi.copyHtmlToClipboard', () async { - final input = 'Example HTML'; + const input = 'Example HTML'; when(mockHostApi.copyHtmlToClipboard(input)).thenReturn(null); await plugin.copyHtmlToClipboard(input); verify(mockHostApi.copyHtmlToClipboard(input)).called(1); @@ -175,7 +174,7 @@ void main() { 'delegates to _hostApi.saveImageToGallery', () async { await plugin.saveImageToGallery(Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -195,7 +194,7 @@ void main() { () async { final imageBytes = Uint8List.fromList([1, 0, 1]); - final options = GalleryImageSaveOptions( + const options = GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'jpg', albumName: 'ExampleAlbum', @@ -207,7 +206,7 @@ void main() { albumName: anyNamed('albumName'), fileExtension: anyNamed('fileExtension'), mimeType: anyNamed('mimeType'), - )).thenAnswer((_) async => null); + )).thenAnswer((_) async {}); await plugin.saveImageToGallery(imageBytes, options: options); @@ -231,7 +230,7 @@ void main() { test( 'passes the mime type correctly to the platform host API', () async { - final options = GalleryImageSaveOptions( + const options = GalleryImageSaveOptions( name: 'ImageName', // IMPORTANT: Use jpg specifically instead of jpeg or png // since the "image/jpg" is invalid and it will verify behavior, @@ -270,7 +269,7 @@ void main() { )).thenThrow(PlatformException(code: errorCode)); expect( plugin.saveImageToGallery(Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -306,7 +305,7 @@ void main() { expect( plugin.saveImageToGallery( Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -325,7 +324,7 @@ void main() { )).thenAnswer((_) async {}); await expectLater( plugin.saveImageToGallery(Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, diff --git a/quill_native_bridge_ios/ios/quill_native_bridge_ios/Package.swift b/quill_native_bridge_ios/ios/quill_native_bridge_ios/Package.swift index 8889d5f..00740a0 100644 --- a/quill_native_bridge_ios/ios/quill_native_bridge_ios/Package.swift +++ b/quill_native_bridge_ios/ios/quill_native_bridge_ios/Package.swift @@ -4,21 +4,21 @@ import PackageDescription let package = Package( - name: "quill_native_bridge_ios", - platforms: [ - .iOS("12.0"), - ], - products: [ - .library(name: "quill-native-bridge-ios", targets: ["quill_native_bridge_ios"]) - ], - dependencies: [], - targets: [ - .target( - name: "quill_native_bridge_ios", - dependencies: [], - resources: [ - .process("Resources"), - ] - ) - ] -) \ No newline at end of file + name: "quill_native_bridge_ios", + platforms: [ + .iOS("12.0") + ], + products: [ + .library(name: "quill-native-bridge-ios", targets: ["quill_native_bridge_ios"]) + ], + dependencies: [], + targets: [ + .target( + name: "quill_native_bridge_ios", + dependencies: [], + resources: [ + .process("Resources") + ] + ) + ] +) diff --git a/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/Messages.g.swift b/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/Messages.g.swift index 94f2285..7c9dc88 100644 --- a/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/Messages.g.swift +++ b/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/Messages.g.swift @@ -26,7 +26,7 @@ final class PigeonError: Error { var localizedDescription: String { return "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" - } + } } private func wrapResult(_ result: Any?) -> [Any?] { @@ -84,7 +84,6 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) } - /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol QuillNativeBridgeApi { func isIosSimulator() throws -> Bool @@ -94,16 +93,24 @@ protocol QuillNativeBridgeApi { func copyImageToClipboard(imageBytes: FlutterStandardTypedData) throws func getClipboardGif() throws -> FlutterStandardTypedData? func openGalleryApp(completion: @escaping (Result) -> Void) - func saveImageToGallery(imageBytes: FlutterStandardTypedData, name: String, albumName: String?, completion: @escaping (Result) -> Void) + func saveImageToGallery( + imageBytes: FlutterStandardTypedData, name: String, albumName: String?, + completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class QuillNativeBridgeApiSetup { static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } /// Sets up an instance of `QuillNativeBridgeApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: QuillNativeBridgeApi?, messageChannelSuffix: String = "") { + static func setUp( + binaryMessenger: FlutterBinaryMessenger, api: QuillNativeBridgeApi?, + messageChannelSuffix: String = "" + ) { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let isIosSimulatorChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.isIosSimulator\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let isIosSimulatorChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.isIosSimulator\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { isIosSimulatorChannel.setMessageHandler { _, reply in do { @@ -116,7 +123,10 @@ class QuillNativeBridgeApiSetup { } else { isIosSimulatorChannel.setMessageHandler(nil) } - let getClipboardHtmlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.getClipboardHtml\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getClipboardHtmlChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.getClipboardHtml\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { getClipboardHtmlChannel.setMessageHandler { _, reply in do { @@ -129,7 +139,10 @@ class QuillNativeBridgeApiSetup { } else { getClipboardHtmlChannel.setMessageHandler(nil) } - let copyHtmlToClipboardChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.copyHtmlToClipboard\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let copyHtmlToClipboardChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.copyHtmlToClipboard\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { copyHtmlToClipboardChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -144,7 +157,10 @@ class QuillNativeBridgeApiSetup { } else { copyHtmlToClipboardChannel.setMessageHandler(nil) } - let getClipboardImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.getClipboardImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getClipboardImageChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.getClipboardImage\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { getClipboardImageChannel.setMessageHandler { _, reply in do { @@ -157,7 +173,10 @@ class QuillNativeBridgeApiSetup { } else { getClipboardImageChannel.setMessageHandler(nil) } - let copyImageToClipboardChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.copyImageToClipboard\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let copyImageToClipboardChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.copyImageToClipboard\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { copyImageToClipboardChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -172,7 +191,10 @@ class QuillNativeBridgeApiSetup { } else { copyImageToClipboardChannel.setMessageHandler(nil) } - let getClipboardGifChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.getClipboardGif\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getClipboardGifChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.getClipboardGif\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { getClipboardGifChannel.setMessageHandler { _, reply in do { @@ -185,7 +207,10 @@ class QuillNativeBridgeApiSetup { } else { getClipboardGifChannel.setMessageHandler(nil) } - let openGalleryAppChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.openGalleryApp\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let openGalleryAppChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.openGalleryApp\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { openGalleryAppChannel.setMessageHandler { _, reply in api.openGalleryApp { result in @@ -200,14 +225,18 @@ class QuillNativeBridgeApiSetup { } else { openGalleryAppChannel.setMessageHandler(nil) } - let saveImageToGalleryChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.saveImageToGallery\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let saveImageToGalleryChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_ios.QuillNativeBridgeApi.saveImageToGallery\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { saveImageToGalleryChannel.setMessageHandler { message, reply in let args = message as! [Any?] let imageBytesArg = args[0] as! FlutterStandardTypedData let nameArg = args[1] as! String let albumNameArg: String? = nilOrValue(args[2]) - api.saveImageToGallery(imageBytes: imageBytesArg, name: nameArg, albumName: albumNameArg) { result in + api.saveImageToGallery(imageBytes: imageBytesArg, name: nameArg, albumName: albumNameArg) { + result in switch result { case .success: reply(wrapResult(nil)) diff --git a/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/QuillNativeBridgeImpl.swift b/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/QuillNativeBridgeImpl.swift index eb58b40..37b0743 100644 --- a/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/QuillNativeBridgeImpl.swift +++ b/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/QuillNativeBridgeImpl.swift @@ -1,181 +1,219 @@ -import Foundation import Flutter +import Foundation import Photos class QuillNativeBridgeImpl: QuillNativeBridgeApi { - func isIosSimulator() throws -> Bool { -#if targetEnvironment(simulator) - return true -#else - return false -#endif + func isIosSimulator() throws -> Bool { + #if targetEnvironment(simulator) + return true + #else + return false + #endif + } + // TODO: Should not hardcode public.html and instead use UTType.html.identifier + + func getClipboardHtml() throws -> String? { + guard let htmlData = UIPasteboard.general.data(forPasteboardType: "public.html") else { + return nil } - // TODO: Should not hardcode public.html and instead use UTType.html.identifier - - func getClipboardHtml() throws -> String? { - guard let htmlData = UIPasteboard.general.data(forPasteboardType: "public.html") else { - return nil - } - let html = String(data: htmlData, encoding: .utf8) - return html + let html = String(data: htmlData, encoding: .utf8) + return html + } + + func copyHtmlToClipboard(html: String) throws { + UIPasteboard.general.setValue(html, forPasteboardType: "public.html") + } + + func copyImageToClipboard(imageBytes: FlutterStandardTypedData) throws { + guard let image = UIImage(data: imageBytes.data) else { + throw PigeonError( + code: "INVALID_IMAGE", message: "Unable to create UIImage from image bytes.", details: nil) } - - func copyHtmlToClipboard(html: String) throws { - UIPasteboard.general.setValue(html, forPasteboardType: "public.html") + UIPasteboard.general.image = image + } + + func getClipboardImage() throws -> FlutterStandardTypedData? { + let pasteboard = UIPasteboard.general + if pasteboard.hasImages { + let image = pasteboard.image + if let imagePngData = image?.pngData() { + return FlutterStandardTypedData(bytes: imagePngData) + } } - - func copyImageToClipboard(imageBytes: FlutterStandardTypedData) throws { - guard let image = UIImage(data: imageBytes.data) else { - throw PigeonError(code: "INVALID_IMAGE", message: "Unable to create UIImage from image bytes.", details: nil) - } - UIPasteboard.general.image = image + if let imageWebpData = pasteboard.data(forPasteboardType: "org.webmproject.webp") { + return FlutterStandardTypedData(bytes: imageWebpData) } - - func getClipboardImage() throws -> FlutterStandardTypedData? { - let pasteboard = UIPasteboard.general - if pasteboard.hasImages { - let image = pasteboard.image - if let imagePngData = image?.pngData() { - return FlutterStandardTypedData(bytes: imagePngData) - } - } - if let imageWebpData = pasteboard.data(forPasteboardType: "org.webmproject.webp") { - return FlutterStandardTypedData(bytes: imageWebpData) - } - return nil + return nil + } + + func getClipboardGif() throws -> FlutterStandardTypedData? { + guard let data = UIPasteboard.general.data(forPasteboardType: "com.compuserve.gif") else { + return nil + } + return FlutterStandardTypedData(bytes: data) + + } + + func openGalleryApp(completion: @escaping (Result) -> Void) { + guard let url = URL(string: "photos-redirect://") else { + completion( + .failure( + PigeonError( + code: "INVALID_URL", message: "The URL scheme is invalid.", + details: "Unable to create a URL for 'photos-redirect://'."))) + return + } + guard UIApplication.shared.canOpenURL(url) else { + completion( + .failure( + PigeonError( + code: "CANNOT_OPEN_URL", message: "Cannot open the Photos app.", + details: + "The device may not have the Photos app installed or it may not support the URL scheme." + ))) + return } - - func getClipboardGif() throws -> FlutterStandardTypedData? { - guard let data = UIPasteboard.general.data(forPasteboardType: "com.compuserve.gif") else { - return nil + UIApplication.shared.open( + url, + completionHandler: { success in + if success { + completion(.success(())) + } else { + completion( + .failure( + PigeonError( + code: "UNKNOWN_ERROR", message: "Failed to open the Photos app.", + details: "An unknown error occurred when trying to open the Photos app."))) } - return FlutterStandardTypedData(bytes: data) - + }) + + } + + func saveImageToGallery( + imageBytes: FlutterStandardTypedData, name: String, albumName: String?, + completion: @escaping (Result) -> Void + ) { + guard UIImage(data: imageBytes.data) != nil else { + completion( + .failure( + PigeonError( + code: "INVALID_IMAGE", message: "Unable to create UIImage from image bytes.", + details: nil))) + return } - - func openGalleryApp(completion: @escaping (Result) -> Void) { - guard let url = URL(string: "photos-redirect://") else { - completion(.failure(PigeonError(code: "INVALID_URL", message: "The URL scheme is invalid.", details: "Unable to create a URL for 'photos-redirect://'."))) + + let needsReadWritePermission = + ProcessInfo.processInfo.operatingSystemVersion.majorVersion < 14 || albumName != nil + + let permissionKey = + needsReadWritePermission + ? "NSPhotoLibraryUsageDescription" : "NSPhotoLibraryAddUsageDescription" + guard let infoPlist = Bundle.main.infoDictionary, + let permissionDescription = infoPlist[permissionKey] as? String + else { + completion( + .failure( + PigeonError( + code: "IOS_INFO_PLIST_NOT_CONFIGURED", + message: + "The iOS `Info.plist` file has not been configured. The key `\(permissionKey)` is not set.", + details: nil + ))) + return + } + + func handlePermissionDenied(status: PHAuthorizationStatus) { + completion( + .failure( + PigeonError( + code: "PERMISSION_DENIED", + message: "The app doesn't have permission to save photos to the gallery.", + details: String(describing: status) + ))) + } + + func isAccessBlocked(status: PHAuthorizationStatus) -> Bool { + return status == .denied || status == .restricted + } + + if #available(iOS 14, *) { + let accessLevel: PHAccessLevel = needsReadWritePermission ? .readWrite : .addOnly + + let currentStatus = PHPhotoLibrary.authorizationStatus(for: accessLevel) + + guard !isAccessBlocked(status: currentStatus) else { + handlePermissionDenied(status: currentStatus) + return + } + + if currentStatus == .notDetermined { + PHPhotoLibrary.requestAuthorization(for: accessLevel) { status in + guard !isAccessBlocked(status: status) else { + handlePermissionDenied(status: status) return + } } - guard UIApplication.shared.canOpenURL(url) else { - completion(.failure(PigeonError(code: "CANNOT_OPEN_URL", message: "Cannot open the Photos app.", details: "The device may not have the Photos app installed or it may not support the URL scheme."))) + } + } else { + // For iOS 13 and previous versions + let currentStatus = PHPhotoLibrary.authorizationStatus() + + guard !isAccessBlocked(status: currentStatus) else { + handlePermissionDenied(status: currentStatus) + return + } + + if currentStatus == .notDetermined { + PHPhotoLibrary.requestAuthorization { status in + guard !isAccessBlocked(status: status) else { + handlePermissionDenied(status: status) return + } } - UIApplication.shared.open(url, completionHandler: { success in - if success { - completion(.success(())) - } else { - completion(.failure(PigeonError(code: "UNKNOWN_ERROR", message: "Failed to open the Photos app.", details: "An unknown error occurred when trying to open the Photos app."))) - } - }) - + } } - - func saveImageToGallery(imageBytes: FlutterStandardTypedData, name: String, albumName: String?, completion: @escaping (Result) -> Void) { - guard UIImage(data: imageBytes.data) != nil else { - completion(.failure(PigeonError(code: "INVALID_IMAGE", message: "Unable to create UIImage from image bytes.", details: nil))) - return - } - - let needsReadWritePermission = ProcessInfo.processInfo.operatingSystemVersion.majorVersion < 14 || albumName != nil - - let permissionKey = needsReadWritePermission ? "NSPhotoLibraryUsageDescription" : "NSPhotoLibraryAddUsageDescription" - guard let infoPlist = Bundle.main.infoDictionary, - let permissionDescription = infoPlist[permissionKey] as? String else { - completion(.failure(PigeonError( - code: "IOS_INFO_PLIST_NOT_CONFIGURED", - message: "The iOS `Info.plist` file has not been configured. The key `\(permissionKey)` is not set.", - details: nil - ))) - return + + // TODO(save-image): Warning "Attempted fetch within change block can trigger deadlock, returning unauthorized fetch result", fix for macOS too. + + PHPhotoLibrary.shared().performChanges({ + let assetRequest = PHAssetCreationRequest.forAsset() + + let options = PHAssetResourceCreationOptions() + options.originalFilename = name + assetRequest.addResource(with: .photo, data: imageBytes.data, options: options) + + if let albumName = albumName { + + let albumFetchOptions = PHFetchOptions() + albumFetchOptions.predicate = NSPredicate(format: "title = %@", albumName) + let existingAlbum = PHAssetCollection.fetchAssetCollections( + with: .album, subtype: .any, options: albumFetchOptions + ).firstObject + + if existingAlbum == nil { + // Create the album + let albumChangeRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection( + withTitle: albumName) + albumChangeRequest.addAssets([assetRequest.placeholderForCreatedAsset] as NSArray) + + } else if let album = existingAlbum { + // Add the image to the existing album + let albumChangeRequest = PHAssetCollectionChangeRequest(for: album) + albumChangeRequest?.addAssets([assetRequest.placeholderForCreatedAsset] as NSArray) } - - func handlePermissionDenied(status: PHAuthorizationStatus) { - completion(.failure(PigeonError( - code: "PERMISSION_DENIED", - message: "The app doesn't have permission to save photos to the gallery.", - details: String(describing: status) + } + }) { success, error in + guard success else { + completion( + .failure( + PigeonError( + code: "SAVE_FAILED", + message: "Failed to save the image to the gallery: \(error?.localizedDescription)", + details: String(describing: error) ))) - } - - func isAccessBlocked(status: PHAuthorizationStatus) -> Bool { - return status == .denied || status == .restricted - } - - if #available(iOS 14, *) { - let accessLevel: PHAccessLevel = needsReadWritePermission ? .readWrite : .addOnly - - let currentStatus = PHPhotoLibrary.authorizationStatus(for: accessLevel) - - guard !isAccessBlocked(status: currentStatus) else { - handlePermissionDenied(status: currentStatus) - return - } - - if currentStatus == .notDetermined { - PHPhotoLibrary.requestAuthorization(for: accessLevel) { status in - guard !isAccessBlocked(status: status) else { - handlePermissionDenied(status: status) - return - } - } - } - } else { - // For iOS 13 and previous versions - let currentStatus = PHPhotoLibrary.authorizationStatus() - - guard !isAccessBlocked(status: currentStatus) else { - handlePermissionDenied(status: currentStatus) - return - } - - if currentStatus == .notDetermined { - PHPhotoLibrary.requestAuthorization { status in - guard !isAccessBlocked(status: status) else { - handlePermissionDenied(status: status) - return - } - } - } - } - - // TODO(save-image): Warning "Attempted fetch within change block can trigger deadlock, returning unauthorized fetch result", fix for macOS too. - - PHPhotoLibrary.shared().performChanges({ - let assetRequest = PHAssetCreationRequest.forAsset() - - let options = PHAssetResourceCreationOptions() - options.originalFilename = name - assetRequest.addResource(with: .photo, data: imageBytes.data, options: options) - - if let albumName = albumName { - - let albumFetchOptions = PHFetchOptions() - albumFetchOptions.predicate = NSPredicate(format: "title = %@", albumName) - let existingAlbum = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: albumFetchOptions).firstObject - - if existingAlbum == nil { - // Create the album - let albumChangeRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: albumName) - albumChangeRequest.addAssets([assetRequest.placeholderForCreatedAsset] as NSArray) - - } else if let album = existingAlbum { - // Add the image to the existing album - let albumChangeRequest = PHAssetCollectionChangeRequest(for: album) - albumChangeRequest?.addAssets([assetRequest.placeholderForCreatedAsset] as NSArray) - } - } - }) { success, error in - guard success else { - completion(.failure(PigeonError( - code: "SAVE_FAILED", - message: "Failed to save the image to the gallery: \(error?.localizedDescription)", - details: String(describing: error) - ))) - return - } - completion(.success(())) - } + return + } + completion(.success(())) } + } } diff --git a/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/QuillNativeBridgePlugin.swift b/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/QuillNativeBridgePlugin.swift index d77cff7..a3a2eb3 100644 --- a/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/QuillNativeBridgePlugin.swift +++ b/quill_native_bridge_ios/ios/quill_native_bridge_ios/Sources/quill_native_bridge_ios/QuillNativeBridgePlugin.swift @@ -2,9 +2,9 @@ import Flutter import UIKit public class QuillNativeBridgePlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let messenger = registrar.messenger() - let api = QuillNativeBridgeImpl() - QuillNativeBridgeApiSetup.setUp(binaryMessenger: messenger, api: api) - } + public static func register(with registrar: FlutterPluginRegistrar) { + let messenger = registrar.messenger() + let api = QuillNativeBridgeImpl() + QuillNativeBridgeApiSetup.setUp(binaryMessenger: messenger, api: api) + } } diff --git a/quill_native_bridge_ios/pubspec.yaml b/quill_native_bridge_ios/pubspec.yaml index cebe98e..4efc0c8 100644 --- a/quill_native_bridge_ios/pubspec.yaml +++ b/quill_native_bridge_ios/pubspec.yaml @@ -19,7 +19,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 pigeon: ^22.6.2 mockito: ^5.4.4 build_runner: ^2.4.13 diff --git a/quill_native_bridge_ios/test/quill_native_bridge_ios_test.dart b/quill_native_bridge_ios/test/quill_native_bridge_ios_test.dart index 0f77c5d..dcac3ab 100644 --- a/quill_native_bridge_ios/test/quill_native_bridge_ios_test.dart +++ b/quill_native_bridge_ios/test/quill_native_bridge_ios_test.dart @@ -5,10 +5,9 @@ import 'package:mockito/mockito.dart'; import 'package:quill_native_bridge_ios/quill_native_bridge_ios.dart'; import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart'; -import 'test_api.g.dart'; - @GenerateMocks([TestQuillNativeBridgeApi]) import 'quill_native_bridge_ios_test.mocks.dart'; +import 'test_api.g.dart'; void main() { // Required when calling TestQuillNativeBridgeApi.setUp() @@ -61,7 +60,7 @@ void main() { verify(mockHostApi.getClipboardHtml()).called(1); expect(nullHtml, isNull); - final exampleHtml = 'An HTML'; + const exampleHtml = 'An HTML'; when(mockHostApi.getClipboardHtml()).thenReturn(exampleHtml); final nonNullHtml = await plugin.getClipboardHtml(); @@ -73,7 +72,7 @@ void main() { test( 'copyHtmlToClipboard delegates to _hostApi.copyHtmlToClipboard', () async { - final input = 'Example HTML'; + const input = 'Example HTML'; when(mockHostApi.copyHtmlToClipboard(input)).thenReturn(null); await plugin.copyHtmlToClipboard(input); verify(mockHostApi.copyHtmlToClipboard(input)).called(1); @@ -141,7 +140,7 @@ void main() { () async { await plugin.saveImageToGallery( Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -159,17 +158,17 @@ void main() { 'passes the arguments correctly to the platform host API', () async { final imageBytes = Uint8List.fromList([1, 0, 1]); - final imageName = 'ExampleImage'; - final imageAlbumName = 'ExampleAlbum'; + const imageName = 'ExampleImage'; + const imageAlbumName = 'ExampleAlbum'; when(mockHostApi.saveImageToGallery( imageBytes, name: anyNamed('name'), albumName: anyNamed('albumName'), - )).thenAnswer((_) async => null); + )).thenAnswer((_) async {}); await plugin.saveImageToGallery(imageBytes, - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: imageName, fileExtension: 'png', albumName: imageAlbumName, @@ -199,7 +198,7 @@ void main() { )).thenThrow(PlatformException(code: errorCode)); expect( plugin.saveImageToGallery(Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -235,7 +234,7 @@ void main() { expect( plugin.saveImageToGallery( Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -252,7 +251,7 @@ void main() { )).thenAnswer((_) async {}); await expectLater( plugin.saveImageToGallery(Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, diff --git a/quill_native_bridge_linux/lib/quill_native_bridge_linux.dart b/quill_native_bridge_linux/lib/quill_native_bridge_linux.dart index e268f82..680df61 100644 --- a/quill_native_bridge_linux/lib/quill_native_bridge_linux.dart +++ b/quill_native_bridge_linux/lib/quill_native_bridge_linux.dart @@ -6,12 +6,12 @@ import 'dart:io' show Process, File hide exitCode; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart'; -import 'package:quill_native_bridge_linux/src/image_saver.dart'; +import 'package:quill_native_bridge_platform_interface/internal.dart'; import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart'; -import 'package:quill_native_bridge_platform_interface/src/image_mime_utils.dart'; import 'src/binary_runner.dart'; import 'src/constants.dart'; +import 'src/image_saver.dart'; import 'src/mime_types_constants.dart'; import 'src/temp_file_utils.dart'; diff --git a/quill_native_bridge_linux/lib/src/binary_runner.dart b/quill_native_bridge_linux/lib/src/binary_runner.dart index f48e1d4..5f1ce88 100644 --- a/quill_native_bridge_linux/lib/src/binary_runner.dart +++ b/quill_native_bridge_linux/lib/src/binary_runner.dart @@ -40,7 +40,7 @@ Future _copyAssetTo({ (await rootBundle.load(assetFilePath)).buffer.asUint8List(); final parentDirectory = destinationFile.parent; - if (!(await parentDirectory.exists())) { + if (!parentDirectory.existsSync()) { await parentDirectory.create(recursive: true); } diff --git a/quill_native_bridge_linux/lib/src/environment_provider.dart b/quill_native_bridge_linux/lib/src/environment_provider.dart index 4fa9c2c..fa77564 100644 --- a/quill_native_bridge_linux/lib/src/environment_provider.dart +++ b/quill_native_bridge_linux/lib/src/environment_provider.dart @@ -10,7 +10,7 @@ abstract class EnvironmentProvider { static EnvironmentProvider get instance => _instance; @visibleForTesting - static void set instance(value) => _instance = value; + static set instance(value) => _instance = value; static void setToDefault() => _instance = DefaultEnvironmentProvider(); } diff --git a/quill_native_bridge_linux/lib/src/image_saver.dart b/quill_native_bridge_linux/lib/src/image_saver.dart index e3a637f..ea18050 100644 --- a/quill_native_bridge_linux/lib/src/image_saver.dart +++ b/quill_native_bridge_linux/lib/src/image_saver.dart @@ -1,6 +1,6 @@ -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_linux/file_selector_linux.dart'; -import 'package:quill_native_bridge_linux/src/environment_provider.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'environment_provider.dart'; class ImageSaver { /// The file selector that's used to prompt the user to choose a directory @@ -18,6 +18,6 @@ class ImageSaver { final userHome = this.userHome; if (userHome == null) return null; if (userHome.isEmpty) return null; - return '${userHome}/${picturesDirectoryName}'; + return '$userHome/$picturesDirectoryName'; } } diff --git a/quill_native_bridge_linux/pubspec.yaml b/quill_native_bridge_linux/pubspec.yaml index c18cfea..311daa9 100644 --- a/quill_native_bridge_linux/pubspec.yaml +++ b/quill_native_bridge_linux/pubspec.yaml @@ -14,14 +14,13 @@ environment: dependencies: flutter: sdk: flutter - quill_native_bridge_platform_interface: ^0.0.1 + quill_native_bridge_platform_interface: ^0.0.1+1 file_selector_linux: ^0.9.1+1 file_selector_platform_interface: ^2.4.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 crypto: ^3.0.5 yaml: ^3.1.2 mockito: ^5.4.4 diff --git a/quill_native_bridge_linux/test/quill_native_bridge_linux_test.dart b/quill_native_bridge_linux/test/quill_native_bridge_linux_test.dart index 807e805..a34f81a 100644 --- a/quill_native_bridge_linux/test/quill_native_bridge_linux_test.dart +++ b/quill_native_bridge_linux/test/quill_native_bridge_linux_test.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_linux/file_selector_linux.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -15,7 +15,7 @@ import 'package:quill_native_bridge_platform_interface/src/image_mime_utils.dart @GenerateMocks([FileSelectorPlatform, EnvironmentProvider]) import 'quill_native_bridge_linux_test.mocks.dart'; -final _fakeLinuxUserHomeDir = '/home/foo-bar/Pictures'; +const _fakeLinuxUserHomeDir = '/home/foo-bar/Pictures'; void main() { late QuillNativeBridgeLinux plugin; @@ -81,7 +81,7 @@ void main() { 'saveImage should return null for file path when user cancels save dialog', () async { final filePath = (await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ))) @@ -151,8 +151,7 @@ void main() { {ImageSaver.linuxUserHomeEnvKey: _fakeLinuxUserHomeDir}); expect( imageSaver.picturesDirectoryPath, - equals( - '${_fakeLinuxUserHomeDir}/${ImageSaver.picturesDirectoryName}'), + equals('$_fakeLinuxUserHomeDir/${ImageSaver.picturesDirectoryName}'), ); }, ); @@ -168,7 +167,7 @@ void main() { )).thenAnswer((_) async => saveLocation); final filePath = (await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ))) @@ -181,7 +180,7 @@ void main() { 'saveImage passes the arguments correctly to fileSelector.getSaveLocation', () async { final imageBytes = Uint8List.fromList([1, 0, 1]); - final options = ImageSaveOptions( + const options = ImageSaveOptions( name: 'ExampleImage', fileExtension: 'jpg', ); @@ -199,9 +198,9 @@ void main() { acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'), )).captured; - SaveDialogOptions? passedOptions = + final SaveDialogOptions passedOptions = capturedOptions[0] as SaveDialogOptions; - List passedAcceptedTypeGroups = + final List passedAcceptedTypeGroups = capturedOptions[1] as List; expect(passedOptions.suggestedName, @@ -220,7 +219,7 @@ void main() { test('saveImage calls fileSelector.getSaveLocation only once', () async { await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', )); @@ -240,7 +239,7 @@ void main() { final imageFilePath = (await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ))) @@ -258,7 +257,7 @@ void main() { options: anyNamed('options'), )).thenAnswer((_) async => null); - final options = ImageSaveOptions( + const options = ImageSaveOptions( name: 'ImageName', // IMPORTANT: Use jpg specifically instead of jpeg or png // since the "image/jpg" is invalid and it will verify behavior, @@ -290,7 +289,7 @@ void main() { reason: 'The $setUp should create the test file'); final filePath = (await plugin.saveImage(imageBytes, - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ))) @@ -312,7 +311,7 @@ void main() { options: anyNamed('acceptedTypeGroups'), )).thenAnswer((_) async => null); final result = await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', )); @@ -324,7 +323,7 @@ void main() { )).thenAnswer((_) async => FileSaveLocation(imageTestFile.path)); final result2 = await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', )); diff --git a/quill_native_bridge_macos/macos/quill_native_bridge_macos/Package.swift b/quill_native_bridge_macos/macos/quill_native_bridge_macos/Package.swift index df6988a..75b05b7 100644 --- a/quill_native_bridge_macos/macos/quill_native_bridge_macos/Package.swift +++ b/quill_native_bridge_macos/macos/quill_native_bridge_macos/Package.swift @@ -4,21 +4,21 @@ import PackageDescription let package = Package( - name: "quill_native_bridge_macos", - platforms: [ - .macOS("10.14") - ], - products: [ - .library(name: "quill-native-bridge-macos", targets: ["quill_native_bridge_macos"]) - ], - dependencies: [], - targets: [ - .target( - name: "quill_native_bridge_macos", - dependencies: [], - resources: [ - .process("Resources") - ] - ) - ] -) \ No newline at end of file + name: "quill_native_bridge_macos", + platforms: [ + .macOS("10.14") + ], + products: [ + .library(name: "quill-native-bridge-macos", targets: ["quill_native_bridge_macos"]) + ], + dependencies: [], + targets: [ + .target( + name: "quill_native_bridge_macos", + dependencies: [], + resources: [ + .process("Resources") + ] + ) + ] +) diff --git a/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/Messages.g.swift b/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/Messages.g.swift index 001061a..a3159e0 100644 --- a/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/Messages.g.swift +++ b/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/Messages.g.swift @@ -26,7 +26,7 @@ final class PigeonError: Error { var localizedDescription: String { return "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" - } + } } private func wrapResult(_ result: Any?) -> [Any?] { @@ -84,7 +84,6 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) } - /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol QuillNativeBridgeApi { func getClipboardHtml() throws -> String? @@ -96,17 +95,27 @@ protocol QuillNativeBridgeApi { func openGalleryApp() throws /// Supports macOS 10.15 and later. func supportsGallerySave() throws -> Bool - func saveImageToGallery(imageBytes: FlutterStandardTypedData, name: String, albumName: String?, completion: @escaping (Result) -> Void) - func saveImage(imageBytes: FlutterStandardTypedData, name: String, fileExtension: String, completion: @escaping (Result) -> Void) + func saveImageToGallery( + imageBytes: FlutterStandardTypedData, name: String, albumName: String?, + completion: @escaping (Result) -> Void) + func saveImage( + imageBytes: FlutterStandardTypedData, name: String, fileExtension: String, + completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class QuillNativeBridgeApiSetup { static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } /// Sets up an instance of `QuillNativeBridgeApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: QuillNativeBridgeApi?, messageChannelSuffix: String = "") { + static func setUp( + binaryMessenger: FlutterBinaryMessenger, api: QuillNativeBridgeApi?, + messageChannelSuffix: String = "" + ) { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let getClipboardHtmlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.getClipboardHtml\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getClipboardHtmlChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.getClipboardHtml\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { getClipboardHtmlChannel.setMessageHandler { _, reply in do { @@ -119,7 +128,10 @@ class QuillNativeBridgeApiSetup { } else { getClipboardHtmlChannel.setMessageHandler(nil) } - let copyHtmlToClipboardChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.copyHtmlToClipboard\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let copyHtmlToClipboardChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.copyHtmlToClipboard\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { copyHtmlToClipboardChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -134,7 +146,10 @@ class QuillNativeBridgeApiSetup { } else { copyHtmlToClipboardChannel.setMessageHandler(nil) } - let getClipboardImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.getClipboardImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getClipboardImageChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.getClipboardImage\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { getClipboardImageChannel.setMessageHandler { _, reply in do { @@ -147,7 +162,10 @@ class QuillNativeBridgeApiSetup { } else { getClipboardImageChannel.setMessageHandler(nil) } - let copyImageToClipboardChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.copyImageToClipboard\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let copyImageToClipboardChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.copyImageToClipboard\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { copyImageToClipboardChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -162,7 +180,10 @@ class QuillNativeBridgeApiSetup { } else { copyImageToClipboardChannel.setMessageHandler(nil) } - let getClipboardGifChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.getClipboardGif\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getClipboardGifChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.getClipboardGif\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { getClipboardGifChannel.setMessageHandler { _, reply in do { @@ -175,7 +196,10 @@ class QuillNativeBridgeApiSetup { } else { getClipboardGifChannel.setMessageHandler(nil) } - let getClipboardFilesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.getClipboardFiles\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getClipboardFilesChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.getClipboardFiles\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { getClipboardFilesChannel.setMessageHandler { _, reply in do { @@ -188,7 +212,10 @@ class QuillNativeBridgeApiSetup { } else { getClipboardFilesChannel.setMessageHandler(nil) } - let openGalleryAppChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.openGalleryApp\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let openGalleryAppChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.openGalleryApp\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { openGalleryAppChannel.setMessageHandler { _, reply in do { @@ -202,7 +229,10 @@ class QuillNativeBridgeApiSetup { openGalleryAppChannel.setMessageHandler(nil) } /// Supports macOS 10.15 and later. - let supportsGallerySaveChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.supportsGallerySave\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let supportsGallerySaveChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.supportsGallerySave\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { supportsGallerySaveChannel.setMessageHandler { _, reply in do { @@ -215,14 +245,18 @@ class QuillNativeBridgeApiSetup { } else { supportsGallerySaveChannel.setMessageHandler(nil) } - let saveImageToGalleryChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.saveImageToGallery\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let saveImageToGalleryChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.saveImageToGallery\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { saveImageToGalleryChannel.setMessageHandler { message, reply in let args = message as! [Any?] let imageBytesArg = args[0] as! FlutterStandardTypedData let nameArg = args[1] as! String let albumNameArg: String? = nilOrValue(args[2]) - api.saveImageToGallery(imageBytes: imageBytesArg, name: nameArg, albumName: albumNameArg) { result in + api.saveImageToGallery(imageBytes: imageBytesArg, name: nameArg, albumName: albumNameArg) { + result in switch result { case .success: reply(wrapResult(nil)) @@ -234,14 +268,18 @@ class QuillNativeBridgeApiSetup { } else { saveImageToGalleryChannel.setMessageHandler(nil) } - let saveImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.saveImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let saveImageChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.quill_native_bridge_macos.QuillNativeBridgeApi.saveImage\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { saveImageChannel.setMessageHandler { message, reply in let args = message as! [Any?] let imageBytesArg = args[0] as! FlutterStandardTypedData let nameArg = args[1] as! String let fileExtensionArg = args[2] as! String - api.saveImage(imageBytes: imageBytesArg, name: nameArg, fileExtension: fileExtensionArg) { result in + api.saveImage(imageBytes: imageBytesArg, name: nameArg, fileExtension: fileExtensionArg) { + result in switch result { case .success(let res): reply(wrapResult(res)) diff --git a/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/QuillNativeBridgeImpl.swift b/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/QuillNativeBridgeImpl.swift index 7cc48de..25f5616 100644 --- a/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/QuillNativeBridgeImpl.swift +++ b/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/QuillNativeBridgeImpl.swift @@ -1,238 +1,292 @@ -import Foundation import FlutterMacOS +import Foundation import Photos -class QuillNativeBridgeImpl: QuillNativeBridgeApi { - func getClipboardHtml() throws -> String? { - guard let htmlData = NSPasteboard.general.data(forType: .html) else { - return nil - } - let html = String(data: htmlData, encoding: .utf8) - return html - } - - func copyHtmlToClipboard(html: String) throws { - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString(html, forType: .html) - } - - func getClipboardImage() throws -> FlutterStandardTypedData? { - // TODO: This can return null when copying an image from some apps (e.g Telegram, Apple notes), seems to work with macOS screenshot and Google Chrome, attemp to fix it later - guard let image = NSPasteboard.general.readObjects(forClasses: [NSImage.self], options: nil)?.first as? NSImage else { - return nil - } - guard let tiffData = image.tiffRepresentation, - let bitmap = NSBitmapImageRep(data: tiffData), - let pngData = bitmap.representation(using: .png, properties: [:]) else { - return nil - } - return FlutterStandardTypedData(bytes: pngData) +class QuillNativeBridgeImpl: QuillNativeBridgeApi { + func getClipboardHtml() throws -> String? { + guard let htmlData = NSPasteboard.general.data(forType: .html) else { + return nil } - - func copyImageToClipboard(imageBytes: FlutterStandardTypedData) throws { - guard let image = NSImage(data: imageBytes.data) else { - throw PigeonError(code: "INVALID_IMAGE", message: "Unable to create NSImage from image bytes.", details: nil) - } - - guard let tiffData = image.tiffRepresentation else { - throw PigeonError(code: "INVALID_IMAGE", message: "Unable to get TIFF representation from NSImage.", details: nil) - } - - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setData(tiffData, forType: .png) - } - - func getClipboardGif() throws -> FlutterStandardTypedData? { - let availableTypes = NSPasteboard.general.types - throw PigeonError(code: "GIF_UNSUPPORTED", message: "Gif image is not supported on macOS. Available types: \(String(describing: availableTypes))", details: nil) - } - - func getClipboardFiles() throws -> [String] { - guard let urlList = NSPasteboard.general.readObjects(forClasses: [NSURL.self], options: nil) as? [NSURL] else { - return [] - } - return urlList.compactMap { url in url.path } + let html = String(data: htmlData, encoding: .utf8) + return html + } + + func copyHtmlToClipboard(html: String) throws { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(html, forType: .html) + } + + func getClipboardImage() throws -> FlutterStandardTypedData? { + // TODO: This can return null when copying an image from some apps (e.g Telegram, Apple notes), seems to work with macOS screenshot and Google Chrome, attemp to fix it later + guard + let image = NSPasteboard.general.readObjects(forClasses: [NSImage.self], options: nil)?.first + as? NSImage + else { + return nil } - - func openGalleryApp() throws { - guard let url = URL(string: "photos://") else { - throw PigeonError(code: "INVALID_URL", message: "The URL scheme is invalid.", details: "Unable to create a URL for 'photos://'.") - } - - let workspace = NSWorkspace.shared - let canOpen = workspace.urlForApplication(toOpen: url) != nil - - guard canOpen else { - throw PigeonError(code: "CANNOT_OPEN_URL", message: "Cannot open the Photos app.", details: "The desktop may not have the Photos app installed or it may not support the URL scheme.") - } - - workspace.open(url) + guard let tiffData = image.tiffRepresentation, + let bitmap = NSBitmapImageRep(data: tiffData), + let pngData = bitmap.representation(using: .png, properties: [:]) + else { + return nil } - - func supportsGallerySave() throws -> Bool { - guard #available(macOS 10.15, *) else { - return false - } - return true + return FlutterStandardTypedData(bytes: pngData) + } + + func copyImageToClipboard(imageBytes: FlutterStandardTypedData) throws { + guard let image = NSImage(data: imageBytes.data) else { + throw PigeonError( + code: "INVALID_IMAGE", message: "Unable to create NSImage from image bytes.", details: nil) } - - func saveImageToGallery(imageBytes: FlutterStandardTypedData, name: String, albumName: String?, completion: @escaping (Result) -> Void) { - guard NSImage(data: imageBytes.data) != nil else { - completion(.failure(PigeonError(code: "INVALID_IMAGE", message: "Unable to create NSImage from image bytes.", details: nil))) - return - } - - let macOSVersion = ProcessInfo.processInfo.operatingSystemVersion - - guard #available(macOS 10.15, *) else { - completion(.failure(PigeonError( - code: "UNSUPPORTED", - message: "Saving images to the system gallery is supported on macOS 10.15 and later.", - details: "The current macOS version is: \(macOSVersion.majorVersion).\(macOSVersion.minorVersion).\(macOSVersion.patchVersion)" - ))) - return + + guard let tiffData = image.tiffRepresentation else { + throw PigeonError( + code: "INVALID_IMAGE", message: "Unable to get TIFF representation from NSImage.", + details: nil) + } + + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setData(tiffData, forType: .png) + } + + func getClipboardGif() throws -> FlutterStandardTypedData? { + let availableTypes = NSPasteboard.general.types + throw PigeonError( + code: "GIF_UNSUPPORTED", + message: + "Gif image is not supported on macOS. Available types: \(String(describing: availableTypes))", + details: nil) + } + + func getClipboardFiles() throws -> [String] { + guard + let urlList = NSPasteboard.general.readObjects(forClasses: [NSURL.self], options: nil) + as? [NSURL] + else { + return [] + } + return urlList.compactMap { url in url.path } + } + + func openGalleryApp() throws { + guard let url = URL(string: "photos://") else { + throw PigeonError( + code: "INVALID_URL", message: "The URL scheme is invalid.", + details: "Unable to create a URL for 'photos://'.") + } + + let workspace = NSWorkspace.shared + let canOpen = workspace.urlForApplication(toOpen: url) != nil + + guard canOpen else { + throw PigeonError( + code: "CANNOT_OPEN_URL", message: "Cannot open the Photos app.", + details: + "The desktop may not have the Photos app installed or it may not support the URL scheme.") + } + + workspace.open(url) + } + + func supportsGallerySave() throws -> Bool { + guard #available(macOS 10.15, *) else { + return false + } + return true + } + + func saveImageToGallery( + imageBytes: FlutterStandardTypedData, name: String, albumName: String?, + completion: @escaping (Result) -> Void + ) { + guard NSImage(data: imageBytes.data) != nil else { + completion( + .failure( + PigeonError( + code: "INVALID_IMAGE", message: "Unable to create NSImage from image bytes.", + details: nil))) + return + } + + let macOSVersion = ProcessInfo.processInfo.operatingSystemVersion + + guard #available(macOS 10.15, *) else { + completion( + .failure( + PigeonError( + code: "UNSUPPORTED", + message: "Saving images to the system gallery is supported on macOS 10.15 and later.", + details: + "The current macOS version is: \(macOSVersion.majorVersion).\(macOSVersion.minorVersion).\(macOSVersion.patchVersion)" + ))) + return + } + + let needsReadWritePermission = macOSVersion.majorVersion < 11 || albumName != nil + + let permissionKey = + needsReadWritePermission + ? "NSPhotoLibraryUsageDescription" : "NSPhotoLibraryAddUsageDescription" + guard let infoPlist = Bundle.main.infoDictionary, + let permissionDescription = infoPlist[permissionKey] as? String + else { + completion( + .failure( + PigeonError( + code: "MACOS_INFO_PLIST_NOT_CONFIGURED", + message: + "The macOS `Info.plist` file has not been configured. The key `\(permissionKey)` is not set.", + details: nil + ))) + return + } + + func handlePermissionDenied(status: PHAuthorizationStatus) { + completion( + .failure( + PigeonError( + code: "PERMISSION_DENIED", + message: "The app doesn't have permission to save photos to the gallery.", + details: String(describing: status) + ))) + } + + func isAccessBlocked(status: PHAuthorizationStatus) -> Bool { + return status == .denied || status == .restricted + } + + Task { + if #available(macOS 11, *) { + let accessLevel: PHAccessLevel = needsReadWritePermission ? .readWrite : .addOnly + + let currentStatus = await PHPhotoLibrary.authorizationStatus(for: accessLevel) + + guard !isAccessBlocked(status: currentStatus) else { + handlePermissionDenied(status: currentStatus) + return } - - let needsReadWritePermission = macOSVersion.majorVersion < 11 || albumName != nil - - let permissionKey = needsReadWritePermission ? "NSPhotoLibraryUsageDescription" : "NSPhotoLibraryAddUsageDescription" - guard let infoPlist = Bundle.main.infoDictionary, - let permissionDescription = infoPlist[permissionKey] as? String else { - completion(.failure(PigeonError( - code: "MACOS_INFO_PLIST_NOT_CONFIGURED", - message: "The macOS `Info.plist` file has not been configured. The key `\(permissionKey)` is not set.", - details: nil - ))) + + if currentStatus == .notDetermined { + let status = await PHPhotoLibrary.requestAuthorization(for: accessLevel) + + guard !isAccessBlocked(status: status) else { + handlePermissionDenied(status: status) return + } } - - func handlePermissionDenied(status: PHAuthorizationStatus) { - completion(.failure(PigeonError( - code: "PERMISSION_DENIED", - message: "The app doesn't have permission to save photos to the gallery.", - details: String(describing: status) - ))) - } - - func isAccessBlocked(status: PHAuthorizationStatus) -> Bool { - return status == .denied || status == .restricted + } else { + // For macOS 10.15 and previous versions + let currentStatus = await PHPhotoLibrary.authorizationStatus() + + guard !isAccessBlocked(status: currentStatus) else { + handlePermissionDenied(status: currentStatus) + return } - - Task { - if #available(macOS 11, *) { - let accessLevel: PHAccessLevel = needsReadWritePermission ? .readWrite : .addOnly - - let currentStatus = await PHPhotoLibrary.authorizationStatus(for: accessLevel) - - guard !isAccessBlocked(status: currentStatus) else { - handlePermissionDenied(status: currentStatus) - return - } - - if currentStatus == .notDetermined { - let status = await PHPhotoLibrary.requestAuthorization(for: accessLevel) - - guard !isAccessBlocked(status: status) else { - handlePermissionDenied(status: status) - return - } - } - } else { - // For macOS 10.15 and previous versions - let currentStatus = await PHPhotoLibrary.authorizationStatus() - - guard !isAccessBlocked(status: currentStatus) else { - handlePermissionDenied(status: currentStatus) - return - } - - if currentStatus == .notDetermined { - PHPhotoLibrary.requestAuthorization { status in - guard !isAccessBlocked(status: status) else { - handlePermissionDenied(status: status) - return - } - } - } - } - - do { - try await PHPhotoLibrary.shared().performChanges({ - let assetRequest = PHAssetCreationRequest.forAsset() - - let options = PHAssetResourceCreationOptions() - options.originalFilename = name - assetRequest.addResource(with: .photo, data: imageBytes.data, options: options) - - if let albumName = albumName { - - let albumFetchOptions = PHFetchOptions() - albumFetchOptions.predicate = NSPredicate(format: "title = %@", albumName) - let existingAlbum = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: albumFetchOptions).firstObject - - if existingAlbum == nil { - // Create the album - let albumChangeRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: albumName) - albumChangeRequest.addAssets([assetRequest.placeholderForCreatedAsset] as NSArray) - - } else if let album = existingAlbum { - // Add the image to the existing album - let albumChangeRequest = PHAssetCollectionChangeRequest(for: album) - albumChangeRequest?.addAssets([assetRequest.placeholderForCreatedAsset] as NSArray) - } - } - }) - completion(.success(())) - } catch { - completion(.failure(PigeonError( - code: "SAVE_FAILED", - message: "Failed to save the image to the gallery: \(error.localizedDescription)", - details: String(describing: error) - ))) + + if currentStatus == .notDetermined { + PHPhotoLibrary.requestAuthorization { status in + guard !isAccessBlocked(status: status) else { + handlePermissionDenied(status: status) + return } + } } + } + + do { + try await PHPhotoLibrary.shared().performChanges({ + let assetRequest = PHAssetCreationRequest.forAsset() + + let options = PHAssetResourceCreationOptions() + options.originalFilename = name + assetRequest.addResource(with: .photo, data: imageBytes.data, options: options) + + if let albumName = albumName { + + let albumFetchOptions = PHFetchOptions() + albumFetchOptions.predicate = NSPredicate(format: "title = %@", albumName) + let existingAlbum = PHAssetCollection.fetchAssetCollections( + with: .album, subtype: .any, options: albumFetchOptions + ).firstObject + + if existingAlbum == nil { + // Create the album + let albumChangeRequest = + PHAssetCollectionChangeRequest.creationRequestForAssetCollection( + withTitle: albumName) + albumChangeRequest.addAssets([assetRequest.placeholderForCreatedAsset] as NSArray) + + } else if let album = existingAlbum { + // Add the image to the existing album + let albumChangeRequest = PHAssetCollectionChangeRequest(for: album) + albumChangeRequest?.addAssets([assetRequest.placeholderForCreatedAsset] as NSArray) + } + } + }) + completion(.success(())) + } catch { + completion( + .failure( + PigeonError( + code: "SAVE_FAILED", + message: "Failed to save the image to the gallery: \(error.localizedDescription)", + details: String(describing: error) + ))) + } + } + } + + func saveImage( + imageBytes: FlutterStandardTypedData, name: String, fileExtension: String, + completion: @escaping (Result) -> Void + ) { + guard + let picturesDirectory = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask) + .first + else { + completion( + .failure( + PigeonError( + code: "DIRECTORY_NOT_FOUND", + message: "Unable to locate the Pictures directory.", + details: "Could not retrieve the user's Pictures directory." + ))) + return + } + + // TODO(save-image) The entitlement com.apple.security.files.user-selected.read-write is required, check if set to avoid crash + let savePanel = NSSavePanel() + savePanel.nameFieldStringValue = "\(name).\(fileExtension)" + savePanel.directoryURL = picturesDirectory + + if #available(macOS 11.0, *) { + savePanel.allowedContentTypes = [.image] + } else { + savePanel.allowedFileTypes = [fileExtension] } - - func saveImage(imageBytes: FlutterStandardTypedData, name: String, fileExtension: String, completion: @escaping (Result) -> Void) { - guard let picturesDirectory = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first else { - completion(.failure(PigeonError( - code: "DIRECTORY_NOT_FOUND", - message: "Unable to locate the Pictures directory.", - details: "Could not retrieve the user's Pictures directory." + + savePanel.begin { result in + guard result == .OK, let selectedUrl = savePanel.url else { + completion(.success(nil)) + return + } + + do { + try imageBytes.data.write(to: selectedUrl) + completion(.success(selectedUrl.path)) + } catch { + completion( + .failure( + PigeonError( + code: "IMAGE_WRITE_FAILED", + message: "Failed to save the image to the specified location.", + details: + "An error occurred while writing the image to \(selectedUrl.path). Error: \(error.localizedDescription)" ))) - return - } - - // TODO(save-image) The entitlement com.apple.security.files.user-selected.read-write is required, check if set to avoid crash - let savePanel = NSSavePanel() - savePanel.nameFieldStringValue = "\(name).\(fileExtension)" - savePanel.directoryURL = picturesDirectory - - if #available(macOS 11.0, *) { - savePanel.allowedContentTypes = [.image] - } else { - savePanel.allowedFileTypes = [fileExtension] - } - - savePanel.begin { result in - guard result == .OK, let selectedUrl = savePanel.url else { - completion(.success(nil)) - return - } - - do { - try imageBytes.data.write(to: selectedUrl) - completion(.success(selectedUrl.path)) - } catch { - completion(.failure(PigeonError( - code: "IMAGE_WRITE_FAILED", - message: "Failed to save the image to the specified location.", - details: "An error occurred while writing the image to \(selectedUrl.path). Error: \(error.localizedDescription)" - ))) - } - } - + } } + + } } diff --git a/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/QuillNativeBridgePlugin.swift b/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/QuillNativeBridgePlugin.swift index 7aeb6a3..73fd1d0 100644 --- a/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/QuillNativeBridgePlugin.swift +++ b/quill_native_bridge_macos/macos/quill_native_bridge_macos/Sources/quill_native_bridge_macos/QuillNativeBridgePlugin.swift @@ -2,9 +2,9 @@ import Cocoa import FlutterMacOS public class QuillNativeBridgePlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let messenger = registrar.messenger - let api = QuillNativeBridgeImpl() - QuillNativeBridgeApiSetup.setUp(binaryMessenger: messenger, api: api) - } + public static func register(with registrar: FlutterPluginRegistrar) { + let messenger = registrar.messenger + let api = QuillNativeBridgeImpl() + QuillNativeBridgeApiSetup.setUp(binaryMessenger: messenger, api: api) + } } diff --git a/quill_native_bridge_macos/pubspec.yaml b/quill_native_bridge_macos/pubspec.yaml index 83e365e..4196968 100644 --- a/quill_native_bridge_macos/pubspec.yaml +++ b/quill_native_bridge_macos/pubspec.yaml @@ -19,7 +19,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 pigeon: ^22.6.2 mockito: ^5.4.4 build_runner: ^2.4.13 diff --git a/quill_native_bridge_macos/test/quill_native_bridge_macos_test.dart b/quill_native_bridge_macos/test/quill_native_bridge_macos_test.dart index abd49e3..81465aa 100644 --- a/quill_native_bridge_macos/test/quill_native_bridge_macos_test.dart +++ b/quill_native_bridge_macos/test/quill_native_bridge_macos_test.dart @@ -5,10 +5,9 @@ import 'package:mockito/mockito.dart'; import 'package:quill_native_bridge_macos/quill_native_bridge_macos.dart'; import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart'; -import 'test_api.g.dart'; - @GenerateMocks([TestQuillNativeBridgeApi]) import 'quill_native_bridge_macos_test.mocks.dart'; +import 'test_api.g.dart'; void main() { // Required when calling TestQuillNativeBridgeApi.setUp() @@ -55,7 +54,7 @@ void main() { verify(mockHostApi.getClipboardHtml()).called(1); expect(nullHtml, isNull); - final exampleHtml = 'An HTML'; + const exampleHtml = 'An HTML'; when(mockHostApi.getClipboardHtml()).thenReturn(exampleHtml); final nonNullHtml = await plugin.getClipboardHtml(); @@ -67,7 +66,7 @@ void main() { test( 'copyHtmlToClipboard delegates to _hostApi.copyHtmlToClipboard', () async { - final input = 'Example HTML'; + const input = 'Example HTML'; when(mockHostApi.copyHtmlToClipboard(input)).thenReturn(null); await plugin.copyHtmlToClipboard(input); verify(mockHostApi.copyHtmlToClipboard(input)).called(1); @@ -170,7 +169,7 @@ void main() { 'delegates to _hostApi.saveImageToGallery', () async { await plugin.saveImageToGallery(Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -187,17 +186,17 @@ void main() { 'passes the arguments correctly to the platform host API', () async { final imageBytes = Uint8List.fromList([1, 0, 1]); - final imageName = 'ExampleImage'; - final imageAlbumName = 'ExampleAlbum'; + const imageName = 'ExampleImage'; + const imageAlbumName = 'ExampleAlbum'; when(mockHostApi.saveImageToGallery( imageBytes, name: anyNamed('name'), albumName: anyNamed('albumName'), - )).thenAnswer((_) async => null); + )).thenAnswer((_) async {}); await plugin.saveImageToGallery(imageBytes, - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: imageName, fileExtension: 'png', albumName: imageAlbumName, @@ -219,7 +218,7 @@ void main() { 'passes the arguments correctly to the platform host API', () async { final imageBytes = Uint8List.fromList([1, 0, 1]); - final options = + const options = ImageSaveOptions(name: 'ExampleImage', fileExtension: 'png'); when(mockHostApi.saveImage( @@ -252,7 +251,7 @@ void main() { )).thenThrow(PlatformException(code: 'UNSUPPORTED')); expect( plugin.saveImageToGallery(Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -274,7 +273,7 @@ void main() { )).thenThrow(PlatformException(code: errorCode)); expect( plugin.saveImageToGallery(Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -315,7 +314,7 @@ void main() { expect( plugin.saveImageToGallery( Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null), ), throwsA(isA() @@ -330,7 +329,7 @@ void main() { await expectLater( plugin.saveImageToGallery( Uint8List.fromList([1, 0, 1]), - options: GalleryImageSaveOptions( + options: const GalleryImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', albumName: null, @@ -345,7 +344,7 @@ void main() { 'saveImage delegates to _hostApi.saveImage', () async { await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', )); @@ -361,20 +360,20 @@ void main() { 'saveImage always passes null to blob URL on non-web platforms', () async { final result = await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', )); expect(result.blobUrl, isNull); - final examplePath = 'path/to/file'; + const examplePath = 'path/to/file'; when(mockHostApi.saveImage( any, name: anyNamed('name'), fileExtension: anyNamed('fileExtension'), )).thenAnswer((_) async => examplePath); final result2 = await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', )); diff --git a/quill_native_bridge_platform_interface/CHANGELOG.md b/quill_native_bridge_platform_interface/CHANGELOG.md index c5df1d5..65f04e6 100644 --- a/quill_native_bridge_platform_interface/CHANGELOG.md +++ b/quill_native_bridge_platform_interface/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## 0.0.1+1 + +- Adds `internal.dart`. + ## 0.0.1 - Improves doc comments. diff --git a/quill_native_bridge_platform_interface/lib/internal.dart b/quill_native_bridge_platform_interface/lib/internal.dart new file mode 100644 index 0000000..9e7f447 --- /dev/null +++ b/quill_native_bridge_platform_interface/lib/internal.dart @@ -0,0 +1,4 @@ +/// Internal APIs for platform implementation packages. +library; + +export 'src/image_mime_utils.dart'; diff --git a/quill_native_bridge_platform_interface/pubspec.yaml b/quill_native_bridge_platform_interface/pubspec.yaml index 18bc289..d38bf0e 100644 --- a/quill_native_bridge_platform_interface/pubspec.yaml +++ b/quill_native_bridge_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: quill_native_bridge_platform_interface description: "A common platform interface for the quill_native_bridge plugin." -version: 0.0.1 +version: 0.0.1+1 homepage: https://github.com/FlutterQuill/quill-native-bridge/tree/main/quill_native_bridge_platform_interface repository: https://github.com/FlutterQuill/quill-native-bridge/tree/main/quill_native_bridge_platform_interface issue_tracker: https://github.com/FlutterQuill/quill-native-bridge/issues @@ -19,4 +19,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 diff --git a/quill_native_bridge_platform_interface/test/image_mime_utils_test.dart b/quill_native_bridge_platform_interface/test/image_mime_utils_test.dart index 6e1b7b0..be60851 100644 --- a/quill_native_bridge_platform_interface/test/image_mime_utils_test.dart +++ b/quill_native_bridge_platform_interface/test/image_mime_utils_test.dart @@ -38,7 +38,7 @@ void main() { test('throws ArgumentError with correct message for unsupported extension', () { - final unsupportedExtension = 'unsupported'; + const unsupportedExtension = 'unsupported'; expect( () => getImageMimeType(unsupportedExtension), diff --git a/quill_native_bridge_platform_interface/test/quill_native_bridge_platform_interface.dart b/quill_native_bridge_platform_interface/test/quill_native_bridge_platform_interface.dart index b04b6a7..2407454 100644 --- a/quill_native_bridge_platform_interface/test/quill_native_bridge_platform_interface.dart +++ b/quill_native_bridge_platform_interface/test/quill_native_bridge_platform_interface.dart @@ -71,7 +71,7 @@ class MockQuillNativeBridgePlatform }) async { savedImageBytes = imageBytes; imageSaveOptions = options; - return ImageSaveResult( + return const ImageSaveResult( filePath: '/path/to/file', blobUrl: 'blob:http://localhost:64030/e58f63d4-2890-469c-9c8e-69e839da6a93', @@ -156,7 +156,7 @@ void main() { test( 'saveImage', () async { - final options = + const options = ImageSaveOptions(name: 'image name', fileExtension: 'png'); final result = await QuillNativeBridgePlatform.instance.saveImage( Uint8List.fromList([9, 3, 5]), @@ -164,7 +164,7 @@ void main() { ); expect( result, - ImageSaveResult( + const ImageSaveResult( filePath: '/path/to/file', blobUrl: 'blob:http://localhost:64030/e58f63d4-2890-469c-9c8e-69e839da6a93', @@ -177,7 +177,7 @@ void main() { test( 'saveImageToGallery', () async { - final galleryImageSaveOptions = GalleryImageSaveOptions( + const galleryImageSaveOptions = GalleryImageSaveOptions( name: 'image name', fileExtension: 'png', albumName: 'example album', diff --git a/quill_native_bridge_web/lib/quill_native_bridge_web.dart b/quill_native_bridge_web/lib/quill_native_bridge_web.dart index 2e39208..89e74c2 100644 --- a/quill_native_bridge_web/lib/quill_native_bridge_web.dart +++ b/quill_native_bridge_web/lib/quill_native_bridge_web.dart @@ -5,9 +5,9 @@ import 'dart:js_interop'; import 'package:flutter/foundation.dart' show Uint8List, debugPrint; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:quill_native_bridge_platform_interface/internal.dart'; import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart'; import 'package:web/web.dart'; -import 'package:quill_native_bridge_platform_interface/src/image_mime_utils.dart'; import 'src/clipboard_api_support_unsafe.dart'; import 'src/mime_types_constants.dart'; diff --git a/quill_native_bridge_web/pubspec.yaml b/quill_native_bridge_web/pubspec.yaml index aca5195..64bb848 100644 --- a/quill_native_bridge_web/pubspec.yaml +++ b/quill_native_bridge_web/pubspec.yaml @@ -17,12 +17,11 @@ dependencies: flutter_web_plugins: sdk: flutter web: ^1.0.0 - quill_native_bridge_platform_interface: ^0.0.1 + quill_native_bridge_platform_interface: ^0.0.1+1 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 flutter: plugin: diff --git a/quill_native_bridge_windows/lib/quill_native_bridge_windows.dart b/quill_native_bridge_windows/lib/quill_native_bridge_windows.dart index 03eb093..a3dde67 100644 --- a/quill_native_bridge_windows/lib/quill_native_bridge_windows.dart +++ b/quill_native_bridge_windows/lib/quill_native_bridge_windows.dart @@ -8,12 +8,12 @@ import 'package:ffi/ffi.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart'; -import 'package:quill_native_bridge_windows/src/image_saver.dart'; import 'package:win32/win32.dart'; import 'src/clipboard_html_format.dart'; import 'src/html_cleaner.dart'; import 'src/html_formatter.dart'; +import 'src/image_saver.dart'; /// A Windows implementation of the [QuillNativeBridgePlatform]. /// diff --git a/quill_native_bridge_windows/lib/src/environment_provider.dart b/quill_native_bridge_windows/lib/src/environment_provider.dart index 4fa9c2c..fa77564 100644 --- a/quill_native_bridge_windows/lib/src/environment_provider.dart +++ b/quill_native_bridge_windows/lib/src/environment_provider.dart @@ -10,7 +10,7 @@ abstract class EnvironmentProvider { static EnvironmentProvider get instance => _instance; @visibleForTesting - static void set instance(value) => _instance = value; + static set instance(value) => _instance = value; static void setToDefault() => _instance = DefaultEnvironmentProvider(); } diff --git a/quill_native_bridge_windows/lib/src/html_formatter.dart b/quill_native_bridge_windows/lib/src/html_formatter.dart index 4af4013..accfbb7 100644 --- a/quill_native_bridge_windows/lib/src/html_formatter.dart +++ b/quill_native_bridge_windows/lib/src/html_formatter.dart @@ -83,7 +83,7 @@ String _formatPosition(int position) { return position.toString().padLeft(4, '0'); } -/// Extracts the content within ... tags from the provided [html]. +/// Extracts the content within ``...`` tags from the provided [html]. /// /// If `` and `` tags are found, the content between them is returned. /// If `` and `` tags are not present, the entire HTML string is returned, diff --git a/quill_native_bridge_windows/lib/src/image_saver.dart b/quill_native_bridge_windows/lib/src/image_saver.dart index ae4e083..09aa198 100644 --- a/quill_native_bridge_windows/lib/src/image_saver.dart +++ b/quill_native_bridge_windows/lib/src/image_saver.dart @@ -1,6 +1,6 @@ import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_windows/file_selector_windows.dart'; -import 'package:quill_native_bridge_windows/src/environment_provider.dart'; +import 'environment_provider.dart'; class ImageSaver { /// The file selector that's used to prompt the user to choose a directory @@ -18,6 +18,6 @@ class ImageSaver { final userHome = this.userHome; if (userHome == null) return null; if (userHome.isEmpty) return null; - return '${userHome}\\${picturesDirectoryName}'; + return '$userHome\\$picturesDirectoryName'; } } diff --git a/quill_native_bridge_windows/pubspec.yaml b/quill_native_bridge_windows/pubspec.yaml index d4b3b89..beabf8b 100644 --- a/quill_native_bridge_windows/pubspec.yaml +++ b/quill_native_bridge_windows/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 mockito: ^5.4.4 build_runner: ^2.4.13 diff --git a/quill_native_bridge_windows/test/quill_native_bridge_windows_test.dart b/quill_native_bridge_windows/test/quill_native_bridge_windows_test.dart index db037d6..7de58e7 100644 --- a/quill_native_bridge_windows/test/quill_native_bridge_windows_test.dart +++ b/quill_native_bridge_windows/test/quill_native_bridge_windows_test.dart @@ -81,7 +81,7 @@ void main() { () async { final result = (await plugin.saveImage( Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ), @@ -153,7 +153,7 @@ void main() { expect( imageSaver.picturesDirectoryPath, equals( - '${_fakeWindowsUserHomeDir}\\${ImageSaver.picturesDirectoryName}'), + '$_fakeWindowsUserHomeDir\\${ImageSaver.picturesDirectoryName}'), ); }, ); @@ -170,7 +170,7 @@ void main() { final filePath = (await plugin.saveImage( Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ), @@ -184,7 +184,7 @@ void main() { 'saveImage passes the arguments correctly to fileSelector.getSaveLocation', () async { final imageBytes = Uint8List.fromList([1, 0, 1]); - final options = ImageSaveOptions( + const options = ImageSaveOptions( name: 'ExampleImage', fileExtension: 'jpg', ); @@ -205,9 +205,9 @@ void main() { acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'), )).captured; - SaveDialogOptions? passedOptions = + final SaveDialogOptions passedOptions = capturedOptions[0] as SaveDialogOptions; - List passedAcceptedTypeGroups = + final List passedAcceptedTypeGroups = capturedOptions[1] as List; expect(passedOptions.suggestedName, @@ -226,7 +226,7 @@ void main() { test('saveImage calls fileSelector.getSaveLocation only once', () async { await plugin.saveImage( Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ), @@ -247,7 +247,7 @@ void main() { final imageFilePath = (await plugin.saveImage( Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ), @@ -273,7 +273,7 @@ void main() { final filePath = (await plugin.saveImage( imageBytes, - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', ), @@ -296,7 +296,7 @@ void main() { options: anyNamed('acceptedTypeGroups'), )).thenAnswer((_) async => null); final result = await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', )); @@ -308,7 +308,7 @@ void main() { )).thenAnswer((_) async => FileSaveLocation(imageTestFile.path)); final result2 = await plugin.saveImage(Uint8List.fromList([1, 0, 1]), - options: ImageSaveOptions( + options: const ImageSaveOptions( name: 'ExampleImage', fileExtension: 'png', )); diff --git a/scripts/format_kotlin.dart b/scripts/format_kotlin.dart new file mode 100644 index 0000000..8dac93a --- /dev/null +++ b/scripts/format_kotlin.dart @@ -0,0 +1,8 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +void main(List args) { + final result = Process.runSync('ktlint', ['--format']); + print(result.stdout); +} diff --git a/scripts/format_swift.dart b/scripts/format_swift.dart new file mode 100644 index 0000000..80a1e9d --- /dev/null +++ b/scripts/format_swift.dart @@ -0,0 +1,15 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +void main(List args) { + const swiftPackages = [ + 'quill_native_bridge_ios/ios/quill_native_bridge_ios', + 'quill_native_bridge_macos/macos/quill_native_bridge_macos', + ]; + for (final swiftPackageDirectory in swiftPackages) { + final result = Process.runSync( + 'swift-format', ['format', '--recursive', '-i', swiftPackageDirectory]); + print(result.stdout); + } +} diff --git a/scripts/packages.dart b/scripts/packages.dart new file mode 100644 index 0000000..cbfeb19 --- /dev/null +++ b/scripts/packages.dart @@ -0,0 +1,11 @@ +// TODO: Use Pub workspaces once the min Dart version is 3.5: https://dart.dev/tools/pub/workspaces +const packages = [ + 'quill_native_bridge', + 'quill_native_bridge_platform_interface', + 'quill_native_bridge_android', + 'quill_native_bridge_ios', + 'quill_native_bridge_linux', + 'quill_native_bridge_macos', + 'quill_native_bridge_web', + 'quill_native_bridge_windows', +]; diff --git a/scripts/pub_get.dart b/scripts/pub_get.dart new file mode 100644 index 0000000..e43f7f1 --- /dev/null +++ b/scripts/pub_get.dart @@ -0,0 +1,9 @@ +import 'dart:io'; + +import 'packages.dart'; + +void main(List args) { + for (final package in packages) { + Process.runSync('flutter', ['pub', 'get', '-C', package]); + } +} diff --git a/scripts/publish_dry_run.dart b/scripts/publish_dry_run.dart new file mode 100644 index 0000000..8c1df5b --- /dev/null +++ b/scripts/publish_dry_run.dart @@ -0,0 +1,13 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'packages.dart'; + +void main(List args) { + for (final package in packages) { + final result = Process.runSync('flutter', ['pub', 'publish', '--dry-run'], + workingDirectory: package); + print(result.stdout); + } +} diff --git a/scripts/test.dart b/scripts/test.dart new file mode 100644 index 0000000..650e1b9 --- /dev/null +++ b/scripts/test.dart @@ -0,0 +1,13 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'packages.dart'; + +void main(List args) { + for (final package in packages) { + final result = + Process.runSync('flutter', ['test'], workingDirectory: package); + print(result.stdout); + } +}