From 1d8ec6c3025fea6d4010118c977da3ddfe162063 Mon Sep 17 00:00:00 2001 From: Ellet Date: Wed, 2 Oct 2024 13:13:00 +0300 Subject: [PATCH 01/18] chore(example): update outdated macOS example using Flutter build macos --config-only, deprecated @NSApplicationMain attribute Signed-off-by: Ellet --- .../example/macos/Runner.xcodeproj/project.pbxproj | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj index 69e3b8961bf..dab7b7c8a99 100644 --- a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -54,6 +54,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 103E92CF1EBAAB82E57C06F1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 2E9F2DE12CD9DE067306B460 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -89,6 +91,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0C0105042ACC016BCC44F609 /* Pods */ = { + isa = PBXGroup; + children = ( + 6D83DCCDFE2A45D91B8A2673 /* Pods-Runner.debug.xcconfig */, + 103E92CF1EBAAB82E57C06F1 /* Pods-Runner.release.xcconfig */, + CD373636C90085FB713CE436 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( @@ -186,6 +199,7 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 47A625D09D635034DC1B5C5E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); From 6ba3c146f4a77b8ad199266c1a8865d1754bc45d Mon Sep 17 00:00:00 2001 From: Ellet Date: Wed, 13 Nov 2024 17:22:06 +0300 Subject: [PATCH 02/18] feat(image_picker_macos): add support for native macOS image picker - Updates the `image_picke_macos`'s `pubspec.yaml` to add `pigeon` as dev dependency and add the `pluginClass` for Swift native code - Adds the `ImagePickerPlugin` in `image_picker_macos/macos` for native macOS plugin with support for SPM and CocoaPods with basic native unit tests - Uses the steps in https://github.com/flutter/flutter/blob/master/docs/ecosystem/testing/Plugin-Tests.md#enabling-xctests-or-xcuitests to enable `XCTests` and `XCUITests` - Updates the `image_picker_macos_test.dart` to fix the test failure and ensure PHPicker is disabled by default - Adds a new button in the example to enable/disable PHPicker macOS implementation and enable the PHPicker by default - Updates the `README.md` of `image_picker_macos` and `image_picker` to document the usage - Removes two TODOs in `image_picker_macos.dart` as they are done with this PR - Adds TODOs that need to be done before merging the PR, some of them are questions, will be removed - Implement the getMultiImageWithOptions() since the getMultiImage is deprecated, updates getMultiImage() to delegate to getMultiImageWithOptions() since getMultiImageWithOptions() is required to access the limit property - Updates the Dart unit tests of image_picker_macos - Adds simple integration test for the example - Updates `pubspec.yaml` and `CHANGELOG.md` of `image_picker` and `image_picker_macos` Signed-off-by: Ellet --- .../image_picker/image_picker/CHANGELOG.md | 3 +- packages/image_picker/image_picker/README.md | 13 +- .../image_picker/image_picker/pubspec.yaml | 2 +- .../image_picker_macos/CHANGELOG.md | 3 +- .../image_picker/image_picker_macos/README.md | 42 +- .../integration_test/image_picker_test.dart | 62 ++ .../image_picker_macos/example/lib/main.dart | 51 ++ .../macos/Runner.xcodeproj/project.pbxproj | 304 +++++++- .../xcshareddata/xcschemes/Runner.xcscheme | 22 + .../RunnerTests/ImageCompressTests.swift | 37 + .../macos/RunnerTests/ImageResizeTests.swift | 236 +++++++ .../macos/RunnerTests/RunnerTests.swift | 94 +++ .../macos/RunnerUITests/RunnerUITests.swift | 32 + .../image_picker_macos/example/pubspec.yaml | 2 + .../example/test_driver/integration_test.dart | 7 + .../lib/image_picker_macos.dart | 134 +++- .../lib/src/messages.g.dart | 297 ++++++++ .../macos/image_picker_macos.podspec | 23 + .../macos/image_picker_macos/Package.swift | 28 + .../image_picker_macos/ImageCompress.swift | 71 ++ .../image_picker_macos/ImagePickerImpl.swift | 519 ++++++++++++++ .../ImagePickerPlugin.swift | 14 + .../image_picker_macos/ImageResize.swift | 94 +++ .../image_picker_macos/Messages.g.swift | 309 ++++++++ .../Resources/PrivacyInfo.xcprivacy | 12 + .../image_picker_macos/pigeons/copyright.txt | 3 + .../image_picker_macos/pigeons/messages.dart | 85 +++ .../image_picker_macos/pubspec.yaml | 4 +- .../test/image_picker_macos_test.dart | 668 +++++++++++++++++- .../test/image_picker_macos_test.mocks.dart | 64 ++ .../image_picker_macos/test/test_api.g.dart | 174 +++++ 31 files changed, 3369 insertions(+), 40 deletions(-) create mode 100644 packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart create mode 100644 packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift create mode 100644 packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift create mode 100644 packages/image_picker/image_picker_macos/example/macos/RunnerTests/RunnerTests.swift create mode 100644 packages/image_picker/image_picker_macos/example/macos/RunnerUITests/RunnerUITests.swift create mode 100644 packages/image_picker/image_picker_macos/example/test_driver/integration_test.dart create mode 100644 packages/image_picker/image_picker_macos/lib/src/messages.g.dart create mode 100644 packages/image_picker/image_picker_macos/macos/image_picker_macos.podspec create mode 100644 packages/image_picker/image_picker_macos/macos/image_picker_macos/Package.swift create mode 100644 packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift create mode 100644 packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift create mode 100644 packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerPlugin.swift create mode 100644 packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageResize.swift create mode 100644 packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift create mode 100644 packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Resources/PrivacyInfo.xcprivacy create mode 100644 packages/image_picker/image_picker_macos/pigeons/copyright.txt create mode 100644 packages/image_picker/image_picker_macos/pigeons/messages.dart create mode 100644 packages/image_picker/image_picker_macos/test/test_api.g.dart diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index ee08d0510f3..3180f9b1e76 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 1.1.3 * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* Updates README to include a reference to the macOS PHPicker feature. ## 1.1.2 diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 866adf58118..ef91fa40023 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -153,7 +153,7 @@ encourage the community to build packages that implement #### macOS installation -Since the macOS implementation uses `file_selector`, you will need to +Since the macOS implementation uses `file_selector` by default, you will need to add a filesystem access [entitlement](https://flutter.dev/to/macos-entitlements): @@ -162,6 +162,17 @@ add a filesystem access ``` +This setup is still required when using the [macOS PHPicker](#macos-phpicker) on **macOS 12 and older versions**, since it's only supported on **macOS 13+** and will fallback to the `file_selector` implementation. + +#### macOS PHPicker + +To use the [macOS native image picker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller) which is supported on **macOS 13 and newer versions**, +refer to the [image_picker_macos PHPicker](https://pub.dev/packages/image_picker_macos#phpicker) section. + +* **on macOS 13 and newer versions**: If this feature is used, the +filesystem access entitlement in the [macOS installation](#macos-installation) is not required. +* **on macOS 12 and older versions**: This feature is unsupported and will fallback to `file_selector` implementation, the filesystem access entitlement in the [macOS installation](#macos-installation) is required. + ### Example diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index d2f49724c88..5830715fed5 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 1.1.2 +version: 1.1.3 environment: sdk: ^3.4.0 diff --git a/packages/image_picker/image_picker_macos/CHANGELOG.md b/packages/image_picker/image_picker_macos/CHANGELOG.md index f1b3bd3f159..0052b547730 100644 --- a/packages/image_picker/image_picker_macos/CHANGELOG.md +++ b/packages/image_picker/image_picker_macos/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.3.0 * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* Adds macOS 13+ PHPicker functionality (optional and disabled by default). ## 0.2.1+1 diff --git a/packages/image_picker/image_picker_macos/README.md b/packages/image_picker/image_picker_macos/README.md index 9aa87453532..d9da5ce328c 100644 --- a/packages/image_picker/image_picker_macos/README.md +++ b/packages/image_picker/image_picker_macos/README.md @@ -2,15 +2,45 @@ A macOS implementation of [`image_picker`][1]. +## PHPicker + +macOS 13.0 and newer versions supports native image picking via [PHPickerViewController][5]. + +To use this feature, add the following code to your app before calling any `image_picker` APIs: + + +```dart +import 'package:image_picker_macos/image_picker_macos.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +// ··· + final ImagePickerPlatform imagePickerImplementation = + ImagePickerPlatform.instance; + if (imagePickerImplementation is ImagePickerMacOS) { + imagePickerImplementation.useMacOSPHPicker = true; + } +``` + +This implementation depends on the photos in the [Photos for macOS App][6], +if the user didn't open the app or import any photos to the app, +they will see: `No photos` or `No Photos or Videos` message even if they +have them as files on their desktop. The macOS Photos app supports importing images from an iOS device. + +> [!NOTE] +> This feature is only supported on **macOS 13.0 and newer versions**, on older versions it will fallback to using [file_selector][3] if enabled. +> By defaults it's disabled on all versions. + ## Limitations `ImageSource.camera` is not supported unless a `cameraDelegate` is set. ### pickImage() -The arguments `maxWidth`, `maxHeight`, and `imageQuality` are not currently supported. + +The arguments `maxWidth`, `maxHeight`, `imageQuality` and `limit` are only supported when using the [PHPicker](#phpicker) implementation; they are not available in the default [file_selector][5] implementation. + +The argument `requestFullMetadata` is unsupported on macOS. ### pickVideo() -The argument `maxDuration` is not currently supported. +The argument `maxDuration` is not supported even when using the [PHPicker](#phpicker) implementation. ## Usage @@ -25,14 +55,18 @@ should add it to your `pubspec.yaml` as usual. ### Entitlements -This package is currently implemented using [`file_selector`][3], so you will -need to add a read-only file acces [entitlement][4]: +This package’s default implementation relies on [file_selector][3], +which requires the following read-only file access entitlement: ```xml com.apple.security.files.user-selected.read-only ``` +If you're using the [PHPicker](#phpicker) and require at **least macOS 13** to run the app, this entitlement is not required. + [1]: https://pub.dev/packages/image_picker [2]: https://flutter.dev/to/endorsed-federated-plugin [3]: https://pub.dev/packages/file_selector [4]: https://flutter.dev/to/macos-entitlements +[5]: https://developer.apple.com/documentation/photokit/phpickerviewcontroller +[6]: https://www.apple.com/in/macos/photos/ diff --git a/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart b/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart new file mode 100644 index 00000000000..921d3448aa1 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:example/main.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_macos/image_picker_macos.dart'; +import 'package:image_picker_macos/src/messages.g.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +ImagePickerMacOS get requireMacOSImplementation { + final ImagePickerPlatform imagePickerImplementation = + ImagePickerPlatform.instance; + if (imagePickerImplementation is! ImagePickerMacOS) { + fail('Expected the implementation to be $ImagePickerMacOS'); + } + return imagePickerImplementation; +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('example', () { + testWidgets( + 'Pressing the PHPicker toggle button updates it correctly', + (WidgetTester tester) async { + final ImagePickerMacOS imagePickerImplementation = + requireMacOSImplementation; + expect(imagePickerImplementation.useMacOSPHPicker, false, + reason: 'The default is to not using PHPicker'); + + await tester.pumpWidget(const MyApp()); + final Finder togglePHPickerFinder = + find.byTooltip('toggle macOS PHPPicker'); + expect(togglePHPickerFinder, findsOneWidget); + + await tester.tap(togglePHPickerFinder); + expect(imagePickerImplementation.useMacOSPHPicker, true, + reason: 'Pressing the toggle button should update it correctly'); + + await tester.tap(togglePHPickerFinder); + expect(imagePickerImplementation.useMacOSPHPicker, false, + reason: 'Pressing the toggle button should update it correctly'); + }, + ); + testWidgets( + 'multi-video selection is not implemented', + (WidgetTester tester) async { + final ImagePickerApi hostApi = ImagePickerApi(); + await expectLater( + hostApi.pickVideos(GeneralOptions(limit: 2)), + throwsA(predicate( + (PlatformException e) => + e.code == 'UNIMPLEMENTED' && + e.message == 'Multi-video selection is not implemented', + )), + ); + }, + ); + }); +} diff --git a/packages/image_picker/image_picker_macos/example/lib/main.dart b/packages/image_picker/image_picker_macos/example/lib/main.dart index 8f4887095c1..76da5f6e7f4 100644 --- a/packages/image_picker/image_picker_macos/example/lib/main.dart +++ b/packages/image_picker/image_picker_macos/example/lib/main.dart @@ -8,11 +8,22 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +// #docregion phpicker-example +import 'package:image_picker_macos/image_picker_macos.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +// #enddocregion phpicker-example import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { + // Set to use macOS PHPicker. + // #docregion phpicker-example + final ImagePickerPlatform imagePickerImplementation = + ImagePickerPlatform.instance; + if (imagePickerImplementation is ImagePickerMacOS) { + imagePickerImplementation.useMacOSPHPicker = true; + } + // #enddocregion phpicker-example runApp(const MyApp()); } @@ -385,6 +396,46 @@ class _MyHomePageState extends State { child: const Icon(Icons.videocam), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + void showSnackbarText(String text) { + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar( + SnackBar(content: Text(text)), + ); + } + + if (_picker is! ImagePickerMacOS) { + throw StateError( + 'Expected the implementation to be $ImagePickerMacOS but was ${_picker.runtimeType}'); + } + + if (_picker.useMacOSPHPicker) { + _picker.useMacOSPHPicker = false; + setState(() {}); + showSnackbarText('Switched to file_picker implementation.'); + } else { + _picker.useMacOSPHPicker = true; + setState(() {}); + showSnackbarText( + 'Switched to macOS PHPPicker implementation.'); + } + }, + tooltip: 'toggle macOS PHPPicker', + child: () { + if (_picker is ImagePickerMacOS) { + return _picker.useMacOSPHPicker + ? const Icon(Icons.apple) + : const Icon(Icons.file_open); + } + throw StateError( + 'Expected the implementation to be $ImagePickerMacOS but was ${_picker.runtimeType}'); + }(), + ), + ), ], ), ); diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj index dab7b7c8a99..28d31a28a61 100644 --- a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,10 @@ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; BBE2B8C47A32673657F9E2DC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B40E0F19CFF2111C7C9F07D /* Pods_Runner.framework */; }; + 7ABC95832CBD9D810004CBA6 /* ImageCompressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */; }; + 7ABC95842CBD9D810004CBA6 /* ImageResizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95802CBD9D810004CBA6 /* ImageResizeTests.swift */; }; + 7ABC95852CBD9D810004CBA6 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95812CBD9D810004CBA6 /* RunnerTests.swift */; }; + 7ABC95892CBD9D8A0004CBA6 /* RunnerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95862CBD9D8A0004CBA6 /* RunnerUITests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -38,6 +42,20 @@ remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; + 7ABC952F2CB979800004CBA6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 7ABC954F2CBAF9680004CBA6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -71,6 +89,12 @@ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 450A6A13EFEFF8F54A1685E1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7ABC952B2CB979800004CBA6 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7ABC95492CBAF9680004CBA6 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompressTests.swift; sourceTree = ""; }; + 7ABC95802CBD9D810004CBA6 /* ImageResizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageResizeTests.swift; sourceTree = ""; }; + 7ABC95812CBD9D810004CBA6 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 7ABC95862CBD9D8A0004CBA6 /* RunnerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerUITests.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 7B40E0F19CFF2111C7C9F07D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; @@ -88,6 +112,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7ABC95282CB979800004CBA6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7ABC95462CBAF9680004CBA6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -98,7 +136,6 @@ 103E92CF1EBAAB82E57C06F1 /* Pods-Runner.release.xcconfig */, CD373636C90085FB713CE436 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -118,6 +155,8 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 7ABC95822CBD9D810004CBA6 /* RunnerTests */, + 7ABC95882CBD9D8A0004CBA6 /* RunnerUITests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, E0DBDFAB26ECD71761DCC07A /* Pods */, @@ -128,6 +167,8 @@ isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* example.app */, + 7ABC952B2CB979800004CBA6 /* RunnerTests.xctest */, + 7ABC95492CBAF9680004CBA6 /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -167,6 +208,24 @@ path = Runner; sourceTree = ""; }; + 7ABC95822CBD9D810004CBA6 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */, + 7ABC95802CBD9D810004CBA6 /* ImageResizeTests.swift */, + 7ABC95812CBD9D810004CBA6 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 7ABC95882CBD9D8A0004CBA6 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + 7ABC95862CBD9D8A0004CBA6 /* RunnerUITests.swift */, + ); + path = RunnerUITests; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -199,7 +258,6 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 47A625D09D635034DC1B5C5E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -214,13 +272,49 @@ productReference = 33CC10ED2044A3C60003C045 /* example.app */; productType = "com.apple.product-type.application"; }; + 7ABC952A2CB979800004CBA6 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7ABC95342CB979800004CBA6 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 7ABC95272CB979800004CBA6 /* Sources */, + 7ABC95282CB979800004CBA6 /* Frameworks */, + 7ABC95292CB979800004CBA6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7ABC95302CB979800004CBA6 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 7ABC952B2CB979800004CBA6 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7ABC95482CBAF9680004CBA6 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7ABC95542CBAF9680004CBA6 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 7ABC95452CBAF9680004CBA6 /* Sources */, + 7ABC95462CBAF9680004CBA6 /* Frameworks */, + 7ABC95472CBAF9680004CBA6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7ABC95502CBAF9680004CBA6 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 7ABC95492CBAF9680004CBA6 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0920; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -238,6 +332,15 @@ CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; + 7ABC952A2CB979800004CBA6 = { + CreatedOnToolsVersion = 16.0; + LastSwiftMigration = 1600; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 7ABC95482CBAF9680004CBA6 = { + CreatedOnToolsVersion = 16.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; @@ -258,6 +361,8 @@ targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + 7ABC952A2CB979800004CBA6 /* RunnerTests */, + 7ABC95482CBAF9680004CBA6 /* RunnerUITests */, ); }; /* End PBXProject section */ @@ -272,6 +377,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7ABC95292CB979800004CBA6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7ABC95472CBAF9680004CBA6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -348,6 +467,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7ABC95272CB979800004CBA6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ABC95832CBD9D810004CBA6 /* ImageCompressTests.swift in Sources */, + 7ABC95842CBD9D810004CBA6 /* ImageResizeTests.swift in Sources */, + 7ABC95852CBD9D810004CBA6 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7ABC95452CBAF9680004CBA6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ABC95892CBD9D8A0004CBA6 /* RunnerUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -356,6 +493,16 @@ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; + 7ABC95302CB979800004CBA6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 7ABC952F2CB979800004CBA6 /* PBXContainerItemProxy */; + }; + 7ABC95502CBAF9680004CBA6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 7ABC954F2CBAF9680004CBA6 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -598,6 +745,137 @@ }; name = Release; }; + 7ABC95312CB979800004CBA6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 7ABC95322CB979800004CBA6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 7ABC95332CB979800004CBA6 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; + 7ABC95512CBAF9680004CBA6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + 7ABC95522CBAF9680004CBA6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; + 7ABC95532CBAF9680004CBA6 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Runner; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -631,6 +909,26 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7ABC95342CB979800004CBA6 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7ABC95312CB979800004CBA6 /* Debug */, + 7ABC95322CB979800004CBA6 /* Release */, + 7ABC95332CB979800004CBA6 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7ABC95542CBAF9680004CBA6 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7ABC95512CBAF9680004CBA6 /* Debug */, + 7ABC95522CBAF9680004CBA6 /* Release */, + 7ABC95532CBAF9680004CBA6 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8b5a3a2814e..1d4cc11b931 100644 --- a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -55,6 +55,28 @@ + + + + + + + + NSImage { + let image = NSImage(size: size) + image.lockFocus() + NSColor.white.set() + NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill() + image.unlockFocus() + return image + } + + func testShouldCompressImage() { + XCTAssertFalse(shouldCompressImage(quality: 100), "Quality 100 should not compress the image.") + XCTAssertTrue(shouldCompressImage(quality: 80), "Quality bellow 100 should compress the image.") + XCTAssertFalse( + shouldCompressImage(quality: nil), "Should not compress the image when the quality is nil.") + } + + func testImageCompression() throws { + let testImage = createTestImage(size: NSSize(width: 100, height: 100)) + + let compressedImage = try testImage.compressed(quality: 80) + + XCTAssertLessThan( + compressedImage.tiffRepresentation!.count, testImage.tiffRepresentation!.count, + "Compressed image data should be smaller than the original image data.") + } + +} diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift new file mode 100644 index 00000000000..a6a331d82eb --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift @@ -0,0 +1,236 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +@testable import image_picker_macos + +final class ImageResizeTests: XCTestCase { + + private func createTestImage(size: NSSize) -> NSImage { + let image = NSImage(size: size) + image.lockFocus() + NSColor.black.set() + NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill() + image.unlockFocus() + return image + } + + func testNilMaxSize() { + let originalImage = createTestImage(size: NSSize(width: 1200, height: 800)) + + let resizedImage = originalImage.resizedOrOriginal(maxSize: nil) + + XCTAssertEqual( + resizedImage, originalImage, "Should return the original image when \(MaxSize.self) is nil.") + } + + func testResizeExceedingMaxSize() { + let originalImage = createTestImage(size: NSSize(width: 1200, height: 800)) + + let maxSize = MaxSize(width: 600, height: 400) + let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize) + + // The resized image should be scaled down to fit within the max size while maintaining the aspect ratio. + XCTAssertEqual( + resizedImage.size.width, maxSize.width, + "Resized image width should not exceed the maximum allowed width.") + XCTAssertEqual( + resizedImage.size.height, maxSize.height, + "Resized image height should not exceed the maximum allowed height.") + } + + func testResizeBelowMaxSize() { + let originalImage = createTestImage(size: NSSize(width: 600, height: 400)) + + let resizedImage = originalImage.resizedOrOriginal(maxSize: MaxSize(width: 1200, height: 800)) + + // The resized image should remain the same size, as it's already smaller than the max size. + XCTAssertEqual( + resizedImage.size.width, originalImage.size.width, + "Resized image width should remain unchanged when smaller than the maximum allowed width.") + XCTAssertEqual( + resizedImage.size.height, originalImage.size.height, + "Resized image height should remain unchanged when smaller than the maximum allowed height.") + } + + func testResizeWidthOnly() { + // An image where only the width exceeds the max size + let originalImage = createTestImage(size: NSSize(width: 600, height: 200)) + + let maxSize = MaxSize(width: 300, height: 400) + let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize) + + // The image should be resized proportionally based on width + XCTAssertEqual( + resizedImage.size.width, maxSize.width, "The width should be equal to max width.") + XCTAssertEqual(resizedImage.size.height, 100, "The height should be resized proportionally.") + } + + func testResizeHeightOnly() { + // An image where only the height exceeds the max size + let originalImage = createTestImage(size: NSSize(width: 400, height: 600)) + + let maxSize = MaxSize(width: 500, height: 300) + let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize) + + // The image should be resized proportionally based on height + XCTAssertEqual(resizedImage.size.width, 200, "The width should be resized proportionally.") + XCTAssertEqual( + resizedImage.size.height, maxSize.height, "The height should be equal to max height.") + } + + func testResizeExtremeAspectRatio() { + // An image (20:1) with an extreme aspect ratio (very wide) + let originalImage = createTestImage(size: NSSize(width: 2000, height: 100)) + + let maxSize = MaxSize(width: 600, height: 400) + let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize) + + // The resized image should be within the max size while maintaining aspect ratio + XCTAssertEqual(resizedImage.size.width, 600, "The width should be resized to max width") + XCTAssertEqual(resizedImage.size.height, 30, "The height should be resized proportionally.") + } + + func testResizeImageWithSameAspectRatio() { + let originalImage = createTestImage(size: NSSize(width: 800, height: 400)) + + let maxSize = MaxSize(width: 600, height: 300) + let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize) + + XCTAssertEqual( + resizedImage.size.width, maxSize.width, + "Width should be equal to max width when the aspect ratio is the same") + XCTAssertEqual( + resizedImage.size.height, maxSize.height, + "Height should be equal to height width when the aspect ratio is the same") + } + + func testResizedOrOriginalWithUndefinedSize() { + let image = createTestImage(size: NSSize(width: 300, height: 200)) + let resizedImage = image.resizedOrOriginal(maxSize: MaxSize()) + + XCTAssertEqual( + image.size.width, resizedImage.size.width, + "Should return the original image without resizing.") + XCTAssertEqual( + image.size.height, resizedImage.size.height, + "Should return the original image without resizing.") + } + + func testShouldResize() { + let imageSize = NSSize(width: 400, height: 600) + let image = NSImage(size: imageSize) + + XCTAssertFalse( + image.shouldResize(maxSize: MaxSize()), + "Should not resize when both the width and height are nil." + ) + + XCTAssertTrue( + image.shouldResize(maxSize: MaxSize(width: 300, height: 500)), + "Should resize when image size larger than max size." + ) + XCTAssertTrue( + image.shouldResize(maxSize: MaxSize(width: 300)), + "Should resize when image width larger than max width." + ) + XCTAssertTrue( + image.shouldResize(maxSize: MaxSize(height: 500)), + "Should resize when image height larger than max height." + ) + + XCTAssertFalse( + image.shouldResize(maxSize: MaxSize(width: 500, height: 700)), + "Should not resize when image size smaller than max size." + ) + XCTAssertFalse( + image.shouldResize(maxSize: MaxSize(width: 500)), + "Should not resize when image width smaller than max width." + ) + XCTAssertFalse( + image.shouldResize(maxSize: MaxSize(height: 700)), + "Should not resize when image height smaller than max height." + ) + + XCTAssertFalse( + image.shouldResize(maxSize: MaxSize(width: imageSize.width, height: imageSize.height)), + "Should not resize when image size equal max size." + ) + + XCTAssertTrue( + image.shouldResize(maxSize: MaxSize(width: 350, height: 700)), + "Should resize when image width larger than max width and image height less than max height." + ) + XCTAssertTrue( + image.shouldResize(maxSize: MaxSize(width: 450, height: 500)), + "Should resize when image height is larger than max height and image width less than max width" + ) + + } + + func testHasAnyDimension() { + XCTAssertFalse( + MaxSize(width: nil, height: nil).hasAnyDimension(), + "Should not resize when both width and height are nil.") + XCTAssertTrue( + MaxSize(width: 20, height: nil).hasAnyDimension(), + "Should resize when width is specified and height is nil.") + XCTAssertTrue( + MaxSize(width: nil, height: 20).hasAnyDimension(), + "Should resize when height is specified and width is nil.") + XCTAssertTrue( + MaxSize(width: 20, height: 20).hasAnyDimension(), + "Should resize when both width and height are specified.") + } + + func testMaxSizeToNSSize_withDefinedWidthAndHeight() { + let image = createTestImage(size: NSSize(width: 50, height: 50)) + let maxSize = MaxSize(width: 32, height: 96) + + XCTAssertEqual( + maxSize.toNSSize(image: image).width, maxSize.width, + "Expected width to match MaxSize width.") + XCTAssertEqual( + maxSize.toNSSize(image: image).height, maxSize.height, + "Expected height to match MaxSize height.") + } + + func testMaxSizeToNSSize_withDefinedWidthOnly() { + let image = createTestImage(size: NSSize(width: 50, height: 50)) + let maxSize = MaxSize(width: 128) + + XCTAssertEqual( + maxSize.toNSSize(image: image).width, maxSize.width, + "Expected width to match MaxSize width.") + XCTAssertEqual( + maxSize.toNSSize(image: image).height, image.size.height, + "Expected height to default to image height when MaxSize height is nil.") + } + + func testMaxSizeToNSSize_withDefinedHeightOnly() { + let image = createTestImage(size: NSSize(width: 50, height: 50)) + let maxSize = MaxSize(height: 64) + + XCTAssertEqual( + maxSize.toNSSize(image: image).width, image.size.width, + "Expected width to default to image width when MaxSize width is nil.") + XCTAssertEqual( + maxSize.toNSSize(image: image).height, maxSize.height, + "Expected height to match MaxSize height.") + } + + func testMaxSizeToNSSize_withUndefinedWidthAndHeight() { + let image = createTestImage(size: NSSize(width: 50, height: 50)) + let maxSize = MaxSize() + + XCTAssertEqual( + maxSize.toNSSize(image: image).width, image.size.width, + "Expected width to default to image width when MaxSize width is nil.") + XCTAssertEqual( + maxSize.toNSSize(image: image).height, image.size.height, + "Expected height to default to image height when MaxSize height is nil.") + } + +} diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000000..6b6becdf324 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,94 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +@testable import image_picker_macos + +final class RunnerTests: XCTestCase { + + func testSupportsPHPicker() { + let imagePicker = ImagePickerImpl() + if #available(macOS 13.0, *) { + XCTAssertTrue( + imagePicker.supportsPHPicker(), + "PHPicker is expected to be supported on macOS 13.0 and newer versions.") + } else { + XCTAssertFalse( + imagePicker.supportsPHPicker(), + "PHPicker is expected to be unsupported on macOS versions older than 13.0.") + } + } + + func testImageFileType() { + XCTAssertEqual( + imageFileType(quality: 100), NSBitmapImageRep.FileType.png, + "Quality 100 should return PNG file type.") + XCTAssertEqual( + imageFileType(quality: 99), NSBitmapImageRep.FileType.jpeg, + "Quality below 100 should return JPEG file type.") + XCTAssertEqual( + imageFileType(quality: nil), NSBitmapImageRep.FileType.png, + "Quality nil should return PNG file type.") + } + + func testImageFileExt() { + XCTAssertEqual( + imageFileExt(fileType: NSBitmapImageRep.FileType.png), "png", + "File extension for PNG should be 'png'.") + XCTAssertEqual( + imageFileExt(fileType: NSBitmapImageRep.FileType.jpeg), "jpeg", + "File extension for JPEG should be 'jpeg'.") + } + + func testGenerateUniqueImageFileName() { + let fileType = NSBitmapImageRep.FileType.jpeg + let generatedFileName = generateUniqueImageFileName(imageFileType: fileType) + let expectedExtension = imageFileExt(fileType: fileType) + + // Extract the UUID part of the generated file name + let uuidStringFromFile = generatedFileName.replacingOccurrences( + of: ".\(expectedExtension)", with: "") + + let fileUUID = UUID(uuidString: uuidStringFromFile) + + XCTAssertNotNil(fileUUID, "Generated file name should start with a valid UUID.") + XCTAssertTrue( + generatedFileName.hasSuffix(".\(expectedExtension)"), + "Generated file name should have a '\(expectedExtension)' extension.") + } + + func testGenerateTempImageFilePath() { + let fileType: NSBitmapImageRep.FileType = NSBitmapImageRep.FileType.png + let filePath = generateTempImageFilePath(imageFileType: fileType) + let fileExists = FileManager.default.fileExists(atPath: filePath.path) + + XCTAssertFalse(fileExists, "The file at path \(filePath) should not exist.") + XCTAssertEqual(filePath.pathExtension, "png", "The file path should have a .png extension.") + XCTAssertTrue( + filePath.absoluteString.hasPrefix(FileManager.default.temporaryDirectory.absoluteString), + "The file path should be in the temporary directory.") + + XCTAssertTrue(filePath.isFileURL, "The generated path should be a file URL.") + + let anotherFilePath = generateTempImageFilePath(imageFileType: fileType) + XCTAssertNotEqual(filePath, anotherFilePath, "The generated file paths should be unique.") + } + + func testPathString() { + let tempDirectory = FileManager.default.temporaryDirectory + let fileURL = tempDirectory.appendingPathComponent("flutter.dart") + + XCTAssertEqual( + fileURL.path, fileURL.pathString(), + "Expected pathString() to match `URL.path` for the current URL.") + + if #available(macOS 13.0, *) { + XCTAssertEqual( + fileURL.path(), fileURL.pathString(), + "Expected pathString() to match `URL.path()` for macOS 13.0 and later.") + } + } + +} diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerUITests/RunnerUITests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerUITests/RunnerUITests.swift new file mode 100644 index 00000000000..df50b4c0812 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/RunnerUITests/RunnerUITests.swift @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import FlutterMacOS +import XCTest + +@testable import image_picker_macos + +/// The specified amount of time for waiting to check if an element exists. +let kElementWaitingTime: TimeInterval = 30 + +final class RunnerUITests: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + app = XCUIApplication() + app.launch() + } + + override func tearDown() { + app.terminate() + } + + @MainActor + func testImagePicker() throws { + // TODO(EchoEllet): Lacks native UI tests https://discord.com/channels/608014603317936148/1300517990957056080/1300518056690188361 + // https://github.com/flutter/flutter/issues/70234 + } +} diff --git a/packages/image_picker/image_picker_macos/example/pubspec.yaml b/packages/image_picker/image_picker_macos/example/pubspec.yaml index 66435a80756..c9b117318cf 100644 --- a/packages/image_picker/image_picker_macos/example/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/example/pubspec.yaml @@ -24,6 +24,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter flutter: uses-material-design: true diff --git a/packages/image_picker/image_picker_macos/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_macos/example/test_driver/integration_test.dart new file mode 100644 index 00000000000..4f10f2a522f --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart index 9e9447a5710..d509d019a3c 100644 --- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -7,6 +7,8 @@ import 'package:file_selector_platform_interface/file_selector_platform_interfac import 'package:flutter/foundation.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'src/messages.g.dart'; + /// The macOS implementation of [ImagePickerPlatform]. /// /// This class implements the `package:image_picker` functionality for @@ -15,6 +17,10 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { /// Constructs a platform implementation. ImagePickerMacOS(); + /// The platform API generated by Pigeon to communicate with native macOS using a method channel. + /// Used only when [useMacOSPHPicker] is `true` for **PHPicker implementation**. + final ImagePickerApi _hostApi = ImagePickerApi(); + /// The file selector used to prompt the user to select images or videos. @visibleForTesting static FileSelectorPlatform fileSelector = FileSelectorMacOS(); @@ -24,6 +30,42 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { ImagePickerPlatform.instance = ImagePickerMacOS(); } + /// Sets [ImagePickerMacOS] to use [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller) + /// which is **only supported on macOS 13.0+**. + /// + /// Will fallback to [file_selector_macos](https://pub.dev/packages/file_selector_macos) + /// if [useMacOSPHPicker] is `false` or the macOS version doesn't support + /// this feature. + /// + /// Currently defaults to `false`. + /// + /// **Note**: This implementation depends on the photos in the [Photos for macOS App](https://www.apple.com/in/macos/photos/), + /// if the user didn't open the app or import any photos to the app, + /// they will see: `No photos` or `No Photos or Videos` message even if they + /// have them as files on their desktop. + /// + /// Supports picking an image, multi-image, video, media, and multiple media. + bool useMacOSPHPicker = false; + + // TODO(EchoEllet): shouldUsePHPicker() and supportsPHPicker() should not be public, avoid using @visibleForTesting + + /// Return `true` if the current macOS version supports [useMacOSPHPicker]. + /// + /// The [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller) + /// is **supported on macOS 13.0+** + @visibleForTesting + Future supportsPHPicker() => _hostApi.supportsPHPicker(); + + /// Returns `true` if [ImagePickerMacOS] should use [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller). + /// + /// See also: + /// + /// * [useMacOSPHPicker] to check whether **PHPicker** should be preferred over [file_selector_macos](https://pub.dev/packages/file_selector_macos). + /// * [supportsPHPicker] to verify if the current macOS version supports **PHPicker**. + @visibleForTesting + Future shouldUsePHPicker() async => + await supportsPHPicker() && useMacOSPHPicker; + // This is soft-deprecated in the platform interface, and is only implemented // for compatibility. Callers should be using getImageFromSource. @override @@ -83,8 +125,9 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { preferredCameraDevice: preferredCameraDevice)); } - // [ImagePickerOptions] options are not currently supported. If any - // of its fields are set, they will be silently ignored. + // [ImagePickerOptions] options are currently only supported when using + // PHPicker implementation. If any of its fields are set, + // they will be silently ignored. // // If source is `ImageSource.camera`, a `StateError` will be thrown // unless a [cameraDelegate] is set. @@ -97,9 +140,18 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { case ImageSource.camera: return super.getImageFromSource(source: source); case ImageSource.gallery: - // TODO(stuartmorgan): Add a native implementation that can use - // PHPickerViewController on macOS 13+, with this as a fallback for - // older OS versions: https://github.com/flutter/flutter/issues/125829. + if (await shouldUsePHPicker()) { + final String? imagePath = (await _hostApi.pickImages( + _imageOptionsToImageSelectionOptions(options), + GeneralOptions(limit: 1), + )) + .firstOrNull; + if (imagePath == null) { + return null; + } + + return XFile(imagePath); + } const XTypeGroup typeGroup = XTypeGroup(uniformTypeIdentifiers: ['public.image']); final XFile? file = await fileSelector @@ -130,6 +182,15 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { preferredCameraDevice: preferredCameraDevice, maxDuration: maxDuration); case ImageSource.gallery: + if (await shouldUsePHPicker()) { + final String? videoPath = + (await _hostApi.pickVideos(GeneralOptions(limit: 1))).firstOrNull; + if (videoPath == null) { + return null; + } + + return XFile(videoPath); + } const XTypeGroup typeGroup = XTypeGroup(uniformTypeIdentifiers: ['public.movie']); final XFile? file = await fileSelector @@ -141,18 +202,41 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { throw UnimplementedError('Unknown ImageSource: $source'); } - // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently - // supported. If any of these arguments are supplied, they will be silently - // ignored. + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getMultiImageWithOptions. @override Future> getMultiImage({ double? maxWidth, double? maxHeight, int? imageQuality, }) async { - // TODO(stuartmorgan): Add a native implementation that can use - // PHPickerViewController on macOS 13+, with this as a fallback for - // older OS versions: https://github.com/flutter/flutter/issues/125829. + return getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + imageQuality: imageQuality, + maxHeight: maxHeight, + maxWidth: maxWidth, + ), + ), + ); + } + + // [MultiImagePickerOptions] options are currently only + // supported when using PHPicker implementation. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMultiImageWithOptions({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + if (await shouldUsePHPicker()) { + final List images = await _hostApi.pickImages( + _imageOptionsToImageSelectionOptions(options.imageOptions), + GeneralOptions( + limit: options.limit ?? 0, + ), + ); + return images.map((String imagePath) => XFile(imagePath)).toList(); + } const XTypeGroup typeGroup = XTypeGroup(uniformTypeIdentifiers: ['public.image']); final List files = await fileSelector @@ -160,11 +244,35 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { return files; } - // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently - // supported. If any of these arguments are supplied, they will be silently + ImageSelectionOptions _imageOptionsToImageSelectionOptions( + ImageOptions imageOptions, + ) { + return ImageSelectionOptions( + quality: imageOptions.imageQuality ?? 100, + maxSize: MaxSize( + width: imageOptions.maxWidth, + height: imageOptions.maxHeight, + ), + ); + } + + // [ImageOptions] options are currently only + // supported when using PHPicker implementation. If any of these arguments are supplied, they will be silently // ignored. @override Future> getMedia({required MediaOptions options}) async { + if (await shouldUsePHPicker()) { + final List images = await _hostApi.pickMedia( + MediaSelectionOptions( + imageSelectionOptions: + _imageOptionsToImageSelectionOptions(options.imageOptions), + ), + GeneralOptions( + limit: options.limit ?? (options.allowMultiple ? 0 : 1), + ), + ); + return images.map((String mediaPath) => XFile(mediaPath)).toList(); + } const XTypeGroup typeGroup = XTypeGroup( label: 'images and videos', extensions: ['public.image', 'public.movie']); diff --git a/packages/image_picker/image_picker_macos/lib/src/messages.g.dart b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart new file mode 100644 index 00000000000..19d2bf53a06 --- /dev/null +++ b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart @@ -0,0 +1,297 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +/// The common options between [ImageSelectionOptions], [VideoSelectionOptions] +/// and [MediaSelectionOptions]. +class GeneralOptions { + GeneralOptions({ + required this.limit, + }); + + /// The value `0` means no limit. + int limit; + + Object encode() { + return [ + limit, + ]; + } + + static GeneralOptions decode(Object result) { + result as List; + return GeneralOptions( + limit: result[0]! as int, + ); + } +} + +/// Represents the maximum size with [width] and [height] dimensions. +class MaxSize { + MaxSize({ + this.width, + this.height, + }); + + double? width; + + double? height; + + Object encode() { + return [ + width, + height, + ]; + } + + static MaxSize decode(Object result) { + result as List; + return MaxSize( + width: result[0] as double?, + height: result[1] as double?, + ); + } +} + +/// Options for image selection and output. +class ImageSelectionOptions { + ImageSelectionOptions({ + this.maxSize, + required this.quality, + }); + + /// If set, the max size that the image should be resized to fit in. + MaxSize? maxSize; + + /// The quality of the output image, from 0-100. + /// + /// 100 indicates original quality. + int quality; + + Object encode() { + return [ + maxSize, + quality, + ]; + } + + static ImageSelectionOptions decode(Object result) { + result as List; + return ImageSelectionOptions( + maxSize: result[0] as MaxSize?, + quality: result[1]! as int, + ); + } +} + +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; + + Object encode() { + return [ + imageSelectionOptions, + ]; + } + + static MediaSelectionOptions decode(Object result) { + result as List; + return MediaSelectionOptions( + imageSelectionOptions: result[0]! as ImageSelectionOptions, + ); + } +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is GeneralOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is MaxSize) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is ImageSelectionOptions) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is MediaSelectionOptions) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return GeneralOptions.decode(readValue(buffer)!); + case 130: + return MaxSize.decode(readValue(buffer)!); + case 131: + return ImageSelectionOptions.decode(readValue(buffer)!); + case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class ImagePickerApi { + /// Constructor for [ImagePickerApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ImagePickerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future supportsPHPicker() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future> pickImages(ImageSelectionOptions options, GeneralOptions generalOptions) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([options, generalOptions]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + /// Currently, multi-video selection is unimplemented. + Future> pickVideos(GeneralOptions generalOptions) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([generalOptions]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future> pickMedia(MediaSelectionOptions options, GeneralOptions generalOptions) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([options, generalOptions]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } +} diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos.podspec b/packages/image_picker/image_picker_macos/macos/image_picker_macos.podspec new file mode 100644 index 00000000000..f493353d77f --- /dev/null +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint image_picker_macos.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'image_picker_macos' + s.version = '0.0.1' + s.summary = 'Flutter plugin that shows an image picker.' + s.description = <<-DESC +A Flutter plugin for picking images from the image library, and taking new pictures with the camera. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/packages' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_macos' } + s.source_files = 'image_picker_macos/Sources/image_picker_macos/**/*.swift' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Package.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Package.swift new file mode 100644 index 00000000000..19042756f6c --- /dev/null +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import PackageDescription + +let package = Package( + name: "image_picker_macos", + platforms: [ + .macOS("10.11") + ], + products: [ + .library(name: "image-picker-macos", targets: ["image_picker_macos"]) + ], + dependencies: [], + targets: [ + .target( + name: "image_picker_macos", + dependencies: [], + resources: [ + .process("Resources") + ] + ) + ] +) diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift new file mode 100644 index 00000000000..5a6dedd58da --- /dev/null +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import FlutterMacOS +import Foundation + +/// Determines if the image should be compressed based on the quality. +/// +/// - Parameter quality: The quality level (0-100). A quality less than 100 indicates compression. +/// - Returns: Whether the image should be compressed. +func shouldCompressImage(quality: Int64?) -> Bool { + return quality != nil && quality != 100 +} + +extension NSImage { + /// Compresses the image to the specified quality. + /// + /// - Parameter quality: The quality of the image (0 to 100). + /// - Returns: An optional `NSImage` that represents the compressed image. + func compressed(quality: Int64) throws -> NSImage { + assert(quality != 100, "Quality 100 means no compression.") + assert(quality >= 0, "Quality can't be negative.") + + guard let tiffData = self.tiffRepresentation, + let bitmapRep = NSBitmapImageRep(data: tiffData) + else { + // TODO(EchoEllet): Is there a convention for the error code? ImageConversionError or IMAGE_CONVERSION_ERROR or image-conversion-error. Update all codes. + throw PigeonError( + code: "ImageConversionError", message: "Failed to convert NSImage to TIFF data.", + details: nil) + } + + // Convert quality from 0-100 to 0.0-1.0 + let compressionQuality = max(0.0, min(1.0, Double(quality) / 100.0)) + + guard + let compressedData = bitmapRep.representation( + using: .jpeg, properties: [.compressionFactor: compressionQuality]) + else { + throw PigeonError( + code: "CompressionError", message: "Failed to compress image.", details: nil) + } + + guard let compressedImage = NSImage(data: compressedData) else { + throw PigeonError( + code: "ImageCreationError", message: "Failed to create NSImage from compressed data.", + details: nil) + } + + return compressedImage + } + + /// Returns the original image or a compressed version based on the specified quality. + /// + /// - Parameter quality: The compression quality as an optional value. + /// If `nil` or if compression is not needed, the original image is returned. + /// - Returns: The original or compressed `NSImage`. + func compressedOrOriginal(quality: Int64?) throws -> NSImage { + if !shouldCompressImage(quality: quality) { + return self + } + assert( + quality != nil, + "The quality expected to be not nil due to check using \(shouldCompressImage).") + guard let quality = quality else { + return self + } + return try compressed(quality: quality) + } +} diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift new file mode 100644 index 00000000000..f6cb5649cee --- /dev/null +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift @@ -0,0 +1,519 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Foundation +import PhotosUI + +/// An implementation of [image_picker](https://pub.dev/packages/image_picker) for macOS using [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller). +/// +/// The package [image_picker_macos](https://pub.dev/packages/image_picker_macos) depends on [file_selector_macos](https://pub.dev/packages/file_selector_macos) +/// for picking images, videos, and media. It has limited support for resizing and compression and uses the system file picker, this implementation is used by the Dart plugin +/// to use [PHPickerViewController](https://developer.apple.com/documentation/photokit/phpickerviewcontroller) which is supported on macOS 13.0+ +/// otherwise fallback to file selector if unsupported or the user prefers the file selector implementation. +class ImagePickerImpl: NSObject, ImagePickerApi { + /// Returns `true` if the current macOS version supports this feature. + /// + /// `PHPicker` is supported on macOS 13.0+. + /// For more information, see [PHPickerViewController](https://developer.apple.com/documentation/photokit/phpickerviewcontroller). + func supportsPHPicker() -> Bool { + guard #available(macOS 13.0, *) else { + return false + } + return true + } + + private var pickImagesDelegate: PickImagesDelegate? + private var pickVideosDelegate: PickVideosDelegate? + private var pickMediaDelegate: PickMediaDelegate? + + func pickImages( + options: ImageSelectionOptions, generalOptions: GeneralOptions, + completion: @escaping (Result<[String], any Error>) -> Void + ) { + guard #available(macOS 13.0, *) else { + completion( + .failure( + PigeonError( + code: "UNSUPPORTED_PHPICKER", + message: + "PHPicker is only supported on macOS 13.0 or newer. Use \(supportsPHPicker) to check.", + details: nil))) + return + } + + var config = PHPickerConfiguration() + config.selectionLimit = Int(generalOptions.limit) + config.filter = .images + + let picker = PHPickerViewController(configuration: config) + + pickImagesDelegate = PickImagesDelegate( + completion: completion, + options: options + ) + picker.delegate = pickImagesDelegate + + showPHPicker( + picker, + noActiveWindow: { + completion( + .failure( + PigeonError( + code: "WINDOW_NOT_FOUND", message: "No active window to present the picker.", + details: nil))) + }) + } + + func pickVideos( + generalOptions: GeneralOptions, completion: @escaping (Result<[String], any Error>) -> Void + ) { + guard #available(macOS 13.0, *) else { + completion( + .failure( + PigeonError( + code: "UNSUPPORTED_PHPICKER", + message: + "PHPicker is only supported on macOS 13.0 or newer. Use \(supportsPHPicker) to check.", + details: nil))) + return + } + + if generalOptions.limit != nil && generalOptions.limit != 1 { + completion( + .failure( + PigeonError( + code: "UNIMPLEMENTED", message: "Multi-video selection is not implemented", details: nil + ))) + return + } + + var config = PHPickerConfiguration() + config.selectionLimit = 1 + config.filter = .videos + + let picker = PHPickerViewController(configuration: config) + pickVideosDelegate = PickVideosDelegate(completion: completion) + picker.delegate = pickVideosDelegate + + showPHPicker( + picker, + noActiveWindow: { + completion( + .failure( + PigeonError( + code: "WINDOW_NOT_FOUND", message: "No active window to present the picker.", + details: nil))) + }) + } + + func pickMedia( + options: MediaSelectionOptions, generalOptions: GeneralOptions, + completion: @escaping (Result<[String], any Error>) -> Void + ) { + guard #available(macOS 13.0, *) else { + completion( + .failure( + PigeonError( + code: "UNSUPPORTED_PHPICKER", + message: + "PHPicker is only supported on macOS 13.0 or newer. Use \(supportsPHPicker) to check.", + details: nil))) + return + } + + var config = PHPickerConfiguration() + config.selectionLimit = Int(generalOptions.limit) + config.filter = PHPickerFilter.any(of: [.images, .videos]) + + let picker = PHPickerViewController(configuration: config) + pickMediaDelegate = PickMediaDelegate(completion: completion, options: options) + picker.delegate = pickMediaDelegate + + showPHPicker( + picker, + noActiveWindow: { + completion( + .failure( + PigeonError( + code: "WINDOW_NOT_FOUND", message: "No active window to present the picker.", + details: nil))) + }) + } + + @available(macOS 13, *) + private func showPHPicker(_ picker: PHPickerViewController, noActiveWindow: @escaping () -> Void) + { + guard let window = NSApplication.shared.keyWindow else { + noActiveWindow() + return + } + // TODO(EchoEllet): IMPORTANT The window size of the picker is smaller than expected, see the video in https://discord.com/channels/608014603317936148/1295165633931120642/1295470850283147335 + window.contentViewController?.presentAsSheet(picker) + } +} + +class PickImagesDelegate: PHPickerViewControllerDelegate { + private let completion: ((Result<[String], any Error>) -> Void) + private let options: ImageSelectionOptions + + init( + completion: @escaping ((Result<[String], any Error>) -> Void), options: ImageSelectionOptions + ) { + self.completion = completion + self.options = options + } + + @available(macOS 13, *) + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(nil) + + if results.isEmpty { + completion(.success([])) + return + } + + var savedFilePaths: [String] = [] + + Task { + for result in results { + let itemProvider = result.itemProvider + guard itemProvider.canLoadObject(ofClass: NSImage.self) else { + completion( + .failure( + PigeonError( + code: "INVALID_SELECTION", message: "One of the selected items is not an image", + details: nil))) + return + } + + guard + let tempImagePath = await PickImageHandler( + completion: completion, options: options + ).processAndSave(itemProvider: itemProvider) + else { return } + savedFilePaths.append(tempImagePath) + } + completion(.success(savedFilePaths)) + } + } +} + +// Currently, multi-video selection is unimplemented. +class PickVideosDelegate: PHPickerViewControllerDelegate { + private let completion: ((Result<[String], any Error>) -> Void) + + init(completion: @escaping ((Result<[String], any Error>) -> Void)) { + self.completion = completion + } + + @available(macOS 13, *) + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(nil) + + guard let itemProvider = results.first?.itemProvider else { + completion(.success([])) + return + } + + let canLoadVideo = itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) + if !canLoadVideo { + completion( + .failure( + PigeonError( + code: "INVALID_SELECTION", message: "The selected item is not a video", details: nil))) + return + } + + Task { + guard + let tempVideoPath = await PickVideoHandler(completion: completion) + .processAndSave(itemProvider: itemProvider) + else { return } + + completion(.success([tempVideoPath])) + } + + } +} + +class PickMediaDelegate: PHPickerViewControllerDelegate { + private let completion: ((Result<[String], any Error>) -> Void) + private let options: MediaSelectionOptions + + init(completion: @escaping (Result<[String], any Error>) -> Void, options: MediaSelectionOptions) + { + self.completion = completion + self.options = options + } + + @available(macOS 13, *) + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(nil) + + if results.isEmpty { + completion(.success([])) + return + } + + var savedFilePaths: [String] = [] + + Task { + for result in results { + let itemProvider = result.itemProvider + + let canLoadImage = itemProvider.canLoadObject(ofClass: NSImage.self) + if canLoadImage { + guard + let tempImagePath = await PickImageHandler( + completion: completion, options: options.imageSelectionOptions + ).processAndSave(itemProvider: itemProvider) + else { return } + savedFilePaths.append(tempImagePath) + } + + let canLoadVideo = itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) + if canLoadVideo { + guard + let tempVideoPath = await PickVideoHandler(completion: completion).processAndSave( + itemProvider: itemProvider) + else { return } + savedFilePaths.append(tempVideoPath) + } + } + + completion(.success(savedFilePaths)) + } + } + +} + +extension NSItemProvider { + @available(macOS 10.15, *) + @MainActor + func loadObject(ofClass: T.Type) async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + loadObject(ofClass: ofClass) { (object, error) in + if let error = error { + continuation.resume(throwing: error) + } else if let object = object as? T { + continuation.resume(returning: object) + } else { + continuation.resume(throwing: NSError(domain: "INVALID_OBJECT", code: -1, userInfo: nil)) + } + } + } + } + @available(macOS 13.0, *) + @MainActor + func loadDataRepresentation(for contentType: UTType) async throws -> Data { + return try await withCheckedThrowingContinuation { continuation in + loadDataRepresentation(for: contentType) { (data, error) in + if let error = error { + continuation.resume(throwing: error) + } else if let data = data as? Data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: NSError(domain: "INVALID_OBJECT", code: -1, userInfo: nil)) + } + } + } + } +} + +/// Gets the appropriate file type based on whether the image should be compressed. +/// +/// - Parameter quality: Determines if the image should be compressed based on the quality. +/// - Returns: The image file type (`png` or `jpeg`). +func imageFileType(quality: Int64?) -> NSBitmapImageRep.FileType { + let shouldCompress = shouldCompressImage(quality: quality) + // TODO(EchoEllet): The picked image can be JPEG even if it can represented as a PNG + return shouldCompress ? NSBitmapImageRep.FileType.jpeg : NSBitmapImageRep.FileType.png +} + +/// Gets the file extension based from the image file type. +/// +/// - Parameter fileType: The image file type. +/// - Returns: The image file extension. +func imageFileExt(fileType: NSBitmapImageRep.FileType) -> String { + assert( + [NSBitmapImageRep.FileType.png, NSBitmapImageRep.FileType.jpeg].contains(fileType), + "Expected the image file type to be either PNG or JPEG." + ) + switch fileType { + case .jpeg: return "jpeg" + case .png: return "png" + default: + fatalError( + "Case is not covered since only PNG and JPEG will be used: \(String(describing: fileType))") + } +} + +/// Generates a unique image file name with a UUID and the specified file type. +/// +/// The file name includes a UUID followed by the appropriate file extension. +/// For example, if the file type is JPEG, the result will be `UUID.jpeg`. +/// +/// - Parameter imageFileType: The file type for determining the extension. +/// - Returns: A unique image file name. +func generateUniqueImageFileName(imageFileType: NSBitmapImageRep.FileType) -> String { + return UUID().uuidString + ".\(imageFileExt(fileType: imageFileType))" +} + +/// Generates a unique file path for a temporary image in the system's temporary directory. +/// +/// - Parameter imageFileType: The file type of the image (e.g., PNG, JPEG). +/// - Returns: A `URL` representing the unique file path for the temporary image. +func generateTempImageFilePath(imageFileType: NSBitmapImageRep.FileType) -> URL { + let tempDirectory = FileManager.default.temporaryDirectory + + let uniqueFileName = generateUniqueImageFileName(imageFileType: imageFileType) + let filePath = tempDirectory.appendingPathComponent(uniqueFileName) + return filePath +} + +/// Shared image handling between `PickImageDelegate` and `PickMediaDelegate`. +class PickImageHandler { + let completion: ((Result<[String], any Error>) -> Void) + let options: ImageSelectionOptions + + init(completion: @escaping (Result<[String], any Error>) -> Void, options: ImageSelectionOptions) + { + self.completion = completion + self.options = options + } + + /// Load an image, process it if needed, copy it to a temporary directory, and return the file path. + /// + /// Returns `nil` if an error occurs, and handles. + @available(macOS 10.15, *) + func processAndSave(itemProvider: NSItemProvider) async -> String? { + do { + let image = try await itemProvider.loadObject(ofClass: NSImage.self) + guard let processedImage = processImage(image) else { return nil } + guard let tempImagePath = copyImageToTempDir(processedImage) else { return nil } + return tempImagePath + } catch { + completion( + .failure( + PigeonError( + code: "IMAGE_LOAD_FAILED", + message: "Error loading image: \(error.localizedDescription)", details: nil))) + return nil + } + } + + /// Copy an image to a temporary directory and return the file path. + /// + /// Returns `nil` if an error occurs, and handles. + private func copyImageToTempDir(_ image: NSImage) -> String? { + let imageFileType = imageFileType(quality: options.quality) + + guard let tiffData = image.tiffRepresentation, + let bitmapRep = NSBitmapImageRep(data: tiffData), + let imageData = bitmapRep.representation(using: imageFileType, properties: [:]) + else { + completion( + .failure( + PigeonError( + code: "IMAGE_CONVERSION_FAILED", message: "Failed to convert NSImage to TIFF data.", + details: nil))) + return nil + } + + let filePath = generateTempImageFilePath(imageFileType: imageFileType) + + do { + try imageData.write(to: filePath) + return filePath.pathString() + } catch { + completion( + .failure( + PigeonError( + code: "IMAGE_SAVE_FAILED", message: "Error saving image to file: \(error)", details: nil + ))) + return nil + } + } + + /// Resize and compress the image if needed, then return the image. + /// + /// Returns `nil` if an error occurs, and handles. + private func processImage(_ image: NSImage) -> NSImage? { + do { + let processedImage = try image.resizedOrOriginal(maxSize: options.maxSize) + .compressedOrOriginal(quality: options.quality) + return processedImage + } catch { + completion( + .failure( + PigeonError( + code: "IMAGE_PROCESSING_FAILED", + message: "Error processing image: \(error.localizedDescription)", details: nil))) + return nil + } + } +} + +/// Shared image handling between `PickVideosDelegate` and `PickMediaDelegate`. +class PickVideoHandler { + let completion: ((Result<[String], any Error>) -> Void) + + init(completion: @escaping (Result<[String], any Error>) -> Void) { + self.completion = completion + } + + @available(macOS 13.0, *) + func processAndSave(itemProvider: NSItemProvider) async -> String? { + do { + let videoType = UTType.movie + let tempVideoFileName = generateUniqueVideoFileName( + videoFileExt: videoType.preferredFilenameExtension ?? "mov") + let tempVideoUrl = FileManager.default.temporaryDirectory.appendingPathComponent( + tempVideoFileName) + + let videoData = await try itemProvider.loadDataRepresentation(for: videoType) + try videoData.write(to: tempVideoUrl) + + let tempVideoPath = tempVideoUrl.pathString() + + return tempVideoPath + } catch { + completion( + .failure( + PigeonError( + code: "VIDEO_LOAD_FAILED", + message: "Error loading a video: \(error.localizedDescription)", details: nil))) + return nil + } + } +} + +/// Generates a unique video file name with a UUID and the specified file type. +/// +/// The file name includes a UUID followed by the appropriate file extension. +/// For example, if the file type is QuickTime movie, the result will be `UUID.mov`. +/// +/// - Parameter videoFileExt: The file extension. +/// - Returns: A unique image file name. +func generateUniqueVideoFileName(videoFileExt: String) -> String { + return UUID().uuidString + ".\(videoFileExt)" +} + +extension URL { + /// Returns the file path as a `String` for the current `URL`. + /// + /// On macOS 13 and later, this method calls `URL.path()`, + /// while for earlier versions it uses the `URL.path` property. + /// + /// Uses `URL.path()` on newer macOS versions to avoid future deprecation warnings for `URL.path`. + /// + /// - Returns: A `String` representing the file path. + func pathString() -> String { + if #available(macOS 13.0, *) { + return self.path() + } else { + return self.path + } + } +} diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerPlugin.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerPlugin.swift new file mode 100644 index 00000000000..fcf46fd735e --- /dev/null +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerPlugin.swift @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +public class ImagePickerPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let messenger = registrar.messenger + let api = ImagePickerImpl() + ImagePickerApiSetup.setUp(binaryMessenger: messenger, api: api) + } +} diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageResize.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageResize.swift new file mode 100644 index 00000000000..9962b2e8a28 --- /dev/null +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageResize.swift @@ -0,0 +1,94 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import FlutterMacOS +import Foundation + +extension NSImage { + /// Resizes the image to fit within the specified max size (width and height), + /// while maintaining the aspect ratio. + /// + /// - Parameter maxSize: The maximum allowed size (width and height). + /// - Returns: A resized `NSImage` that fits within the max dimensions. + func resized(maxSize: NSSize) -> NSImage { + let originalSize = self.size + + let widthScale = maxSize.width / originalSize.width + let heightScale = maxSize.height / originalSize.height + + let scaleFactor = min(widthScale, heightScale) + + let newSize = NSSize( + width: originalSize.width * scaleFactor, + height: originalSize.height * scaleFactor + ) + + let resizedImage = NSImage(size: newSize, flipped: false) { rect in + self.draw( + in: rect, from: NSRect(origin: .zero, size: originalSize), operation: .sourceOver, + fraction: 1.0) + return true + } + return resizedImage + } + + /// Returns the image resized to fit within the specified maximum size. + /// + /// If the image needs resizing based on `maxSize`, it is resized while maintaining + /// its aspect ratio. Otherwise, the original image is returned. + /// + /// - Parameter maxSize: The maximum width and height for the image. Return the original image if `nil`. + /// - Returns: A resized `NSImage` or the original image. + func resizedOrOriginal(maxSize: MaxSize?) -> NSImage { + guard let maxSize = maxSize else { + return self + } + return shouldResize(maxSize: maxSize) + ? self.resized(maxSize: maxSize.toNSSize(image: self)) : self + } + + /// Checks if the image needs resizing based on the provided max size. + /// Returns `false` if the max size has no dimensions or if the image is within the limits. + /// + /// - Parameter maxSize: The maximum allowable size for the image. + /// - Returns: `true` if the image exceeds either the max width or height; otherwise, `false`. + func shouldResize(maxSize: MaxSize) -> Bool { + if !maxSize.hasAnyDimension() { + return false + } + let imageSize = self.size + + if let maxWidth = maxSize.width, imageSize.width > maxWidth { + return true + } + if let maxHeight = maxSize.height, imageSize.height > maxHeight { + return true + } + + // No resizing needed if both dimensions are within the limits + return false + } +} + +extension MaxSize { + /// Returns `true` if either width or height is not nil. + func hasAnyDimension() -> Bool { + return self.width != nil || self.height != nil + } + + /// Converts a `MaxSize`, which contains optional width and height values, + /// into a non-optional `NSSize`. If either the width or height is not provided (`nil`), + /// It defaults to the original image size. + /// + /// - Parameter image: An `NSImage` used to provide default width and height values + /// if the corresponding dimensions in `MaxSize` are not defined. + /// - Returns: A `NSSize` with the appropriate width and height (non-optional). + func toNSSize(image: NSImage) -> NSSize { + let imageSize = image.size + return NSSize( + width: self.width ?? imageSize.width, + height: self.height ?? imageSize.width + ) + } +} diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift new file mode 100644 index 00000000000..1e0880d7299 --- /dev/null +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift @@ -0,0 +1,309 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Any? + + init(code: String, message: String?, details: Any?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +/// The common options between [ImageSelectionOptions], [VideoSelectionOptions] +/// and [MediaSelectionOptions]. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct GeneralOptions { + /// The value `0` means no limit. + var limit: Int64 + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> GeneralOptions? { + let limit = pigeonVar_list[0] as! Int64 + + return GeneralOptions( + limit: limit + ) + } + func toList() -> [Any?] { + return [ + limit + ] + } +} + +/// Represents the maximum size with [width] and [height] dimensions. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct MaxSize { + var width: Double? = nil + var height: Double? = nil + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> MaxSize? { + let width: Double? = nilOrValue(pigeonVar_list[0]) + let height: Double? = nilOrValue(pigeonVar_list[1]) + + return MaxSize( + width: width, + height: height + ) + } + func toList() -> [Any?] { + return [ + width, + height, + ] + } +} + +/// Options for image selection and output. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct ImageSelectionOptions { + /// If set, the max size that the image should be resized to fit in. + var maxSize: MaxSize? = nil + /// The quality of the output image, from 0-100. + /// + /// 100 indicates original quality. + var quality: Int64 + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ImageSelectionOptions? { + let maxSize: MaxSize? = nilOrValue(pigeonVar_list[0]) + let quality = pigeonVar_list[1] as! Int64 + + return ImageSelectionOptions( + maxSize: maxSize, + quality: quality + ) + } + func toList() -> [Any?] { + return [ + maxSize, + quality, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct MediaSelectionOptions { + var imageSelectionOptions: ImageSelectionOptions + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> MediaSelectionOptions? { + let imageSelectionOptions = pigeonVar_list[0] as! ImageSelectionOptions + + return MediaSelectionOptions( + imageSelectionOptions: imageSelectionOptions + ) + } + func toList() -> [Any?] { + return [ + imageSelectionOptions + ] + } +} + +private class messagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return GeneralOptions.fromList(self.readValue() as! [Any?]) + case 130: + return MaxSize.fromList(self.readValue() as! [Any?]) + case 131: + return ImageSelectionOptions.fromList(self.readValue() as! [Any?]) + case 132: + return MediaSelectionOptions.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class messagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? GeneralOptions { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? MaxSize { + super.writeByte(130) + super.writeValue(value.toList()) + } else if let value = value as? ImageSelectionOptions { + super.writeByte(131) + super.writeValue(value.toList()) + } else if let value = value as? MediaSelectionOptions { + super.writeByte(132) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class messagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return messagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return messagesPigeonCodecWriter(data: data) + } +} + +class messagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = messagesPigeonCodec(readerWriter: messagesPigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol ImagePickerApi { + func supportsPHPicker() throws -> Bool + func pickImages(options: ImageSelectionOptions, generalOptions: GeneralOptions, completion: @escaping (Result<[String], Error>) -> Void) + /// Currently, multi-video selection is unimplemented. + func pickVideos(generalOptions: GeneralOptions, completion: @escaping (Result<[String], Error>) -> Void) + func pickMedia(options: MediaSelectionOptions, generalOptions: GeneralOptions, completion: @escaping (Result<[String], Error>) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class ImagePickerApiSetup { + static var codec: FlutterStandardMessageCodec { messagesPigeonCodec.shared } + /// Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ImagePickerApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let supportsPHPickerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + supportsPHPickerChannel.setMessageHandler { _, reply in + do { + let result = try api.supportsPHPicker() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + supportsPHPickerChannel.setMessageHandler(nil) + } + let pickImagesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pickImagesChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let optionsArg = args[0] as! ImageSelectionOptions + let generalOptionsArg = args[1] as! GeneralOptions + api.pickImages(options: optionsArg, generalOptions: generalOptionsArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + pickImagesChannel.setMessageHandler(nil) + } + /// Currently, multi-video selection is unimplemented. + let pickVideosChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pickVideosChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let generalOptionsArg = args[0] as! GeneralOptions + api.pickVideos(generalOptions: generalOptionsArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + pickVideosChannel.setMessageHandler(nil) + } + let pickMediaChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pickMediaChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let optionsArg = args[0] as! MediaSelectionOptions + let generalOptionsArg = args[1] as! GeneralOptions + api.pickMedia(options: optionsArg, generalOptions: generalOptionsArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + pickMediaChannel.setMessageHandler(nil) + } + } +} diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Resources/PrivacyInfo.xcprivacy b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..c88e30ff906 --- /dev/null +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,12 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + \ No newline at end of file diff --git a/packages/image_picker/image_picker_macos/pigeons/copyright.txt b/packages/image_picker/image_picker_macos/pigeons/copyright.txt new file mode 100644 index 00000000000..1236b63caf3 --- /dev/null +++ b/packages/image_picker/image_picker_macos/pigeons/copyright.txt @@ -0,0 +1,3 @@ +Copyright 2013 The Flutter Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. diff --git a/packages/image_picker/image_picker_macos/pigeons/messages.dart b/packages/image_picker/image_picker_macos/pigeons/messages.dart new file mode 100644 index 00000000000..d8fc53a6c01 --- /dev/null +++ b/packages/image_picker/image_picker_macos/pigeons/messages.dart @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/test_api.g.dart', + swiftOut: + 'macos/image_picker_macos/Sources/image_picker_macos/messages.g.swift', + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// The common options between [ImageSelectionOptions], [VideoSelectionOptions] +/// and [MediaSelectionOptions]. +class GeneralOptions { + GeneralOptions({required this.limit}); + + /// The value `0` means no limit. + int limit; +} + +/// Represents the maximum size with [width] and [height] dimensions. +class MaxSize { + MaxSize(this.width, this.height); + double? width; + double? height; +} + +/// Options for image selection and output. +class ImageSelectionOptions { + ImageSelectionOptions({this.maxSize, required this.quality}); + + /// If set, the max size that the image should be resized to fit in. + MaxSize? maxSize; + + /// The quality of the output image, from 0-100. + /// + /// 100 indicates original quality. + int quality; +} + +// TODO(EchoEllet): Confirm if it's not possible to support maxDurationSeconds with macOS PHPicker +// /// Options for video selection and output. +// class VideoSelectionOptions { +// VideoSelectionOptions(); + +// } + +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; +} + +@HostApi(dartHostTestHandler: 'TestHostImagePickerApi') +abstract class ImagePickerApi { + bool supportsPHPicker(); + + // TODO(EchoEllet): Should ImagePickerApi be more similar to image_picker_ios or image_picker_android messages.dart? + // `pickImage()` and `pickMultiImage()` vs `pickImages()` with `limit` and `allowMultiple`. + // Currently it's closer to the image_picker_android messages.dart but without allowMultiple + + // Return file paths + + @async + List pickImages( + ImageSelectionOptions options, + GeneralOptions generalOptions, + ); + + /// Currently, multi-video selection is unimplemented. + @async + List pickVideos( + GeneralOptions generalOptions, + ); + @async + List pickMedia( + MediaSelectionOptions options, + GeneralOptions generalOptions, + ); +} diff --git a/packages/image_picker/image_picker_macos/pubspec.yaml b/packages/image_picker/image_picker_macos/pubspec.yaml index d5f7181c903..01a70774726 100644 --- a/packages/image_picker/image_picker_macos/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_macos description: macOS platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.1+1 +version: 0.3.0 environment: sdk: ^3.4.0 @@ -14,6 +14,7 @@ flutter: platforms: macos: dartPluginClass: ImagePickerMacOS + pluginClass: ImagePickerPlugin dependencies: file_selector_macos: ^0.9.1+1 @@ -27,6 +28,7 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.4.4 + pigeon: ^22.6.1 topics: - image-picker diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart index 7e94161d4a4..4cad84bb996 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -5,13 +5,15 @@ import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_macos/image_picker_macos.dart'; +import 'package:image_picker_macos/src/messages.g.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'image_picker_macos_test.mocks.dart'; +import 'test_api.g.dart'; -@GenerateMocks([FileSelectorPlatform]) +@GenerateMocks([FileSelectorPlatform, TestHostImagePickerApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -23,10 +25,12 @@ void main() { late ImagePickerMacOS plugin; late MockFileSelectorPlatform mockFileSelectorPlatform; + late MockTestHostImagePickerApi mockImagePickerApi; setUp(() { plugin = ImagePickerMacOS(); mockFileSelectorPlatform = MockFileSelectorPlatform(); + mockImagePickerApi = MockTestHostImagePickerApi(); when(mockFileSelectorPlatform.openFile( acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) @@ -36,14 +40,78 @@ void main() { acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) .thenAnswer((_) async => List.empty()); + when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => false); + + when(mockImagePickerApi.pickImages(any, any)) + .thenAnswer((_) async => []); + + when(mockImagePickerApi.pickVideos(any)) + .thenAnswer((_) async => []); + + when(mockImagePickerApi.pickMedia(any, any)) + .thenAnswer((_) async => []); + ImagePickerMacOS.fileSelector = mockFileSelectorPlatform; + TestHostImagePickerApi.setUp(mockImagePickerApi); }); + void testWithPHPicker({ + required bool enabled, + required void Function() body, + }) { + plugin.useMacOSPHPicker = enabled; + when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => enabled); + body(); + } + test('registered instance', () { ImagePickerMacOS.registerWith(); expect(ImagePickerPlatform.instance, isA()); }); + test('defaults to not using macOS PHPicker', () async { + expect(plugin.useMacOSPHPicker, false); + }); + + test( + 'supportsPHPicker delegate to the supportsPHPicker from the platform API', + () async { + when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => false); + expect(await plugin.supportsPHPicker(), false); + + when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => true); + expect(await plugin.supportsPHPicker(), true); + }, + ); + + test( + 'shouldUsePHPicker returns true when useMacOSPHPicker and supportsPHPicker are true', + () async { + plugin.useMacOSPHPicker = true; + when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => true); + expect(await plugin.shouldUsePHPicker(), true); + }); + + test( + 'shouldUsePHPPicker returns false when either useMacOSPHPicker or supportsPHPicker is false', + () async { + plugin.useMacOSPHPicker = false; + when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => true); + expect(await plugin.shouldUsePHPicker(), false); + + plugin.useMacOSPHPicker = true; + when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => false); + expect(await plugin.shouldUsePHPicker(), false); + }); + + test( + 'shouldUsePHPPicker returns false when both useMacOSPHPicker and supportsPHPicker are false', + () async { + plugin.useMacOSPHPicker = false; + when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => false); + expect(await plugin.shouldUsePHPicker(), false); + }); + group('images', () { test('pickImage passes the accepted type groups correctly', () async { await plugin.pickImage(source: ImageSource.gallery); @@ -74,21 +142,46 @@ void main() { }); test('getImageFromSource calls delegate when source is camera', () async { - const String fakePath = '/tmp/foo'; - plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); - expect( - (await plugin.getImageFromSource(source: ImageSource.camera))!.path, - fakePath); + Future sharedTest() async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getImageFromSource(source: ImageSource.camera))!.path, + fakePath); + } + + // Camera is unsupported on both PHPicker and file_selector, + // ensure always to use the camera delegate + testWithPHPicker(enabled: false, body: sharedTest); + testWithPHPicker(enabled: true, body: sharedTest); }); test( 'getImageFromSource throws StateError when source is camera with no delegate', () async { - await expectLater(plugin.getImageFromSource(source: ImageSource.camera), - throwsStateError); + Future sharedTest() async { + await expectLater(plugin.getImageFromSource(source: ImageSource.camera), + throwsStateError); + } + + // Camera is unsupported on both PHPicker and file_selector, + // ensure always to throw state error + testWithPHPicker(enabled: false, body: sharedTest); + testWithPHPicker(enabled: true, body: sharedTest); }); - test('getMultiImage passes the accepted type groups correctly', () async { + test( + 'getMultiImage delegate to getMultiImageWithOptions', + () async { + // The getMultiImage is soft-deprecated in the platform interface + // and is only implemented for compatibility. Callers should be using getMultiImageWithOptions. + await plugin.getMultiImage(); + verify(plugin.getMultiImageWithOptions()).called(1); + }, + ); + + test('getMultiImageWithOptions passes the accepted type groups correctly', + () async { await plugin.getMultiImage(); final VerificationResult result = verify( @@ -97,9 +190,284 @@ void main() { expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, ['public.image']); }); + + test( + 'getMultiImageWithOptions uses PHPicker when it is enabled', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getMultiImageWithOptions(); + verify(plugin.shouldUsePHPicker()).called(1); + verify(mockImagePickerApi.pickImages(any, any)).called(1); + + verifyNever(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))); + }, + ); + }, + ); + + test( + 'getMultiImageWithOptions uses file selector when PHPicker is disabled', + () async { + testWithPHPicker( + enabled: false, + body: () async { + await plugin.getMultiImageWithOptions(); + verify(plugin.shouldUsePHPicker()).called(1); + verifyNever(mockImagePickerApi.pickImages(any, any)); + + verify(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .called(1); + }, + ); + }, + ); + + test( + 'getMultiImageWithOptions pass 0 as limit to pickImages for PHPicker implementation when unspecified', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getMultiImageWithOptions( + // ignore: avoid_redundant_argument_values + options: const MultiImagePickerOptions(limit: null), + ); + verify(mockImagePickerApi.pickImages( + any, + argThat( + predicate( + (GeneralOptions options) => options.limit == 0), + ), + )); + }, + ); + }, + ); + + test( + 'getImageFromSource uses PHPicker when it is enabled', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getImageFromSource(source: ImageSource.gallery); + verify(plugin.shouldUsePHPicker()).called(1); + verify(mockImagePickerApi.pickImages(any, any)).called(1); + + verifyNever(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))); + }, + ); + }, + ); + + test( + 'getImageFromSource uses file selector when PHPicker is disabled', + () async { + testWithPHPicker( + enabled: false, + body: () async { + await plugin.getImageFromSource(source: ImageSource.gallery); + verify(plugin.shouldUsePHPicker()).called(1); + verifyNever(mockImagePickerApi.pickImages(any, any)); + + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .called(1); + }, + ); + }, + ); + + test( + 'getImageFromSource pass 1 as limit to pickImages for PHPicker implementation', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getImageFromSource(source: ImageSource.gallery); + + verify(mockImagePickerApi.pickImages( + any, + argThat( + predicate( + (GeneralOptions options) => options.limit == 1), + ), + )).called(1); + }, + ); + }, + ); + + test( + 'getImageFromSource uses 100 as image quality if not provided', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getImageFromSource( + source: ImageSource.gallery, + // ignore: avoid_redundant_argument_values + options: const ImagePickerOptions(imageQuality: null), + ); + + verify(mockImagePickerApi.pickImages( + argThat( + predicate( + (ImageSelectionOptions options) => options.quality == 100), + ), + any, + )).called(1); + }, + ); + }, + ); + + test( + 'getMultiImageWithOptions uses 100 as image quality if not provided', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getMultiImageWithOptions( + // ignore: avoid_redundant_argument_values + options: const MultiImagePickerOptions( + // ignore: avoid_redundant_argument_values + imageOptions: ImageOptions(imageQuality: null), + ), + ); + + verify(mockImagePickerApi.pickImages( + argThat( + predicate( + (ImageSelectionOptions options) => options.quality == 100), + ), + any, + )).called(1); + }, + ); + }, + ); + + test( + 'getImageFromSource return the file from the platform API for PHPicker implementation', + () { + testWithPHPicker( + enabled: true, + body: () async { + final List filePaths = ['path/to/file']; + when(mockImagePickerApi.pickImages( + any, + any, + )).thenAnswer((_) async { + return filePaths; + }); + expect( + (await plugin.pickImage(source: ImageSource.gallery))?.path, + filePaths.first, + ); + }, + ); + }, + ); + + test( + 'getMultiImageWithOptions return the file from the platform API for PHPicker implementation', + () { + testWithPHPicker( + enabled: true, + body: () async { + final List filePaths = [ + '/foo/bar/image.png', + '/dev/flutter/plugins/video.mp4', + 'path/to/file' + ]; + when(mockImagePickerApi.pickImages( + any, + any, + )).thenAnswer((_) async { + return filePaths; + }); + expect( + (await plugin.getMultiImageWithOptions()) + .map((XFile file) => file.path), + filePaths, + ); + }, + ); + }, + ); + + test( + 'getImageFromSource passes the arguments correctly to the platform API for the PHPicker implementation', + () { + testWithPHPicker( + enabled: true, + body: () async { + const ImagePickerOptions imageOptions = ImagePickerOptions( + imageQuality: 50, + maxHeight: 40, + maxWidth: 30, + ); + await plugin.getImageFromSource( + source: ImageSource.gallery, options: imageOptions); + verify(mockImagePickerApi.pickImages( + argThat(predicate( + (ImageSelectionOptions options) => + options.maxSize?.width == imageOptions.maxWidth && + options.maxSize?.height == imageOptions.maxHeight && + options.quality == imageOptions.imageQuality, + )), + argThat(predicate( + (GeneralOptions options) => options.limit == 1, + )), + )); + }, + ); + }, + ); + + test( + 'getMultiImageWithOptions passes the arguments correctly to the platform API for the PHPicker implementation', + () { + testWithPHPicker( + enabled: true, + body: () async { + const MultiImagePickerOptions multiImageOptions = + MultiImagePickerOptions( + imageOptions: + ImageOptions(imageQuality: 50, maxHeight: 40, maxWidth: 30), + limit: 50, + ); + await plugin.getMultiImageWithOptions(options: multiImageOptions); + + verify(mockImagePickerApi.pickImages( + argThat(predicate( + (ImageSelectionOptions options) => + options.maxSize?.width == + multiImageOptions.imageOptions.maxWidth && + options.maxSize?.height == + multiImageOptions.imageOptions.maxHeight && + options.quality == + multiImageOptions.imageOptions.imageQuality, + )), + argThat(predicate( + (GeneralOptions options) => + options.limit == multiImageOptions.limit, + )), + )); + }, + ); + }, + ); }); group('videos', () { + // TODO(EchoEllet): (Nit) Should this uses getVideo() instead of the soft-deprecated pickVideo() for consistency? test('pickVideo passes the accepted type groups correctly', () async { await plugin.pickVideo(source: ImageSource.gallery); @@ -119,17 +487,107 @@ void main() { }); test('getVideo calls delegate when source is camera', () async { - const String fakePath = '/tmp/foo'; - plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); - expect( - (await plugin.getVideo(source: ImageSource.camera))!.path, fakePath); + Future sharedTest() async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect((await plugin.getVideo(source: ImageSource.camera))!.path, + fakePath); + } + + // Camera is unsupported on both PHPicker and file_selector, + // ensure always to use the camera delegate + testWithPHPicker(enabled: false, body: sharedTest); + testWithPHPicker(enabled: true, body: sharedTest); }); test('getVideo throws StateError when source is camera with no delegate', () async { - await expectLater( - plugin.getVideo(source: ImageSource.camera), throwsStateError); + Future sharedTest() async { + await expectLater( + plugin.getVideo(source: ImageSource.camera), throwsStateError); + } + + // Camera is unsupported on both PHPicker and file_selector, + // ensure always to throw state error + testWithPHPicker(enabled: false, body: sharedTest); + testWithPHPicker(enabled: true, body: sharedTest); }); + + test( + 'getVideo uses PHPicker when it is enabled', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getVideo(source: ImageSource.gallery); + verify(plugin.shouldUsePHPicker()).called(1); + verify(mockImagePickerApi.pickVideos(any)).called(1); + + verifyNever(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))); + }, + ); + }, + ); + + // TODO(EchoEllet): Improve the test names for this and all related in this file + test( + 'getVideo uses file selector when PHPicker is disabled', + () async { + testWithPHPicker( + enabled: false, + body: () async { + await plugin.getVideo(source: ImageSource.gallery); + verify(plugin.shouldUsePHPicker()).called(1); + verifyNever(mockImagePickerApi.pickVideos(any)); + + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .called(1); + }, + ); + }, + ); + + test( + 'getVideo pass 1 as limit to pickVideos for PHPicker implementation', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getVideo(source: ImageSource.gallery); + + verify(mockImagePickerApi.pickVideos( + argThat( + predicate( + (GeneralOptions options) => options.limit == 1), + ), + )).called(1); + }, + ); + }, + ); + + test( + 'getVideo return the file from the platform API for PHPicker implementation', + () { + testWithPHPicker( + enabled: true, + body: () async { + final List filePaths = ['path/to/file']; + when(mockImagePickerApi.pickVideos( + any, + )).thenAnswer((_) async { + return filePaths; + }); + expect( + (await plugin.getVideo(source: ImageSource.gallery))?.path, + filePaths.first, + ); + }, + ); + }, + ); }); group('media', () { @@ -162,6 +620,186 @@ void main() { ), []); }); + + test( + 'getMedia uses file selector when PHPicker is disabled', + () async { + testWithPHPicker( + enabled: false, + body: () async { + Future sharedTest({required bool allowMultiple}) async { + await plugin.getMedia( + options: MediaOptions(allowMultiple: allowMultiple)); + verify(plugin.shouldUsePHPicker()).called(1); + verifyNever(mockImagePickerApi.pickMedia(any, any)); + + if (allowMultiple) { + verify(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .called(1); + } else { + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .called(1); + } + } + + await sharedTest(allowMultiple: true); + await sharedTest(allowMultiple: false); + }, + ); + }, + ); + + test( + 'getMedia uses PHPicker when it is enabled', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getMedia( + options: const MediaOptions(allowMultiple: false), + ); + verify(plugin.shouldUsePHPicker()).called(1); + verify(mockImagePickerApi.pickMedia(any, any)).called(1); + + verifyNever(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))); + }, + ); + }, + ); + + test( + 'getMultiImageWithOptions pass 0 as limit to pickImages when unspecified ' + 'and 1 if allowMultiple is false for PHPicker implementation', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + // ignore: avoid_redundant_argument_values + limit: null, + ), + ); + verify(mockImagePickerApi.pickMedia( + any, + argThat( + predicate( + (GeneralOptions options) => options.limit == 0), + ), + )); + + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + // ignore: avoid_redundant_argument_values + limit: null, + ), + ); + verify(mockImagePickerApi.pickMedia( + any, + argThat( + predicate( + (GeneralOptions options) => options.limit == 1), + ), + )); + }, + ); + }, + ); + + test( + 'getMedia return the files from the platform API for PHPicker implementation', + () { + testWithPHPicker( + enabled: true, + body: () async { + final List filePaths = [ + '/foo/bar/image.png', + '/dev/flutter/plugins/video.mp4', + 'path/to/file' + ]; + when(mockImagePickerApi.pickMedia( + any, + any, + )).thenAnswer((_) async { + return filePaths; + }); + expect( + (await plugin.getMedia( + options: const MediaOptions(allowMultiple: true))) + .map((XFile file) => file.path), + filePaths, + ); + }, + ); + }, + ); + + test( + 'getMedia uses 100 as image quality if not provided', + () async { + testWithPHPicker( + enabled: true, + body: () async { + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + // ignore: avoid_redundant_argument_values + imageOptions: ImageOptions(imageQuality: null), + ), + ); + + verify(mockImagePickerApi.pickMedia( + argThat( + predicate( + (MediaSelectionOptions options) => + options.imageSelectionOptions.quality == 100), + ), + any, + )).called(1); + }, + ); + }, + ); + + test( + 'getMedia passes the arguments correctly to the platform API for the PHPicker implementation', + () { + testWithPHPicker( + enabled: true, + body: () async { + const MediaOptions mediaOptions = MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions( + maxWidth: 500, + maxHeight: 300, + imageQuality: 80, + ), + limit: 10, + ); + await plugin.getMedia(options: mediaOptions); + verify(mockImagePickerApi.pickMedia( + argThat(predicate( + (MediaSelectionOptions options) => + options.imageSelectionOptions.maxSize?.width == + mediaOptions.imageOptions.maxWidth && + options.imageSelectionOptions.maxSize?.height == + mediaOptions.imageOptions.maxHeight && + options.imageSelectionOptions.quality == + mediaOptions.imageOptions.imageQuality, + )), + argThat(predicate( + (GeneralOptions options) => options.limit == mediaOptions.limit, + )), + )); + }, + ); + }, + ); }); } diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart index 0887befdb0b..71019b86c8c 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart @@ -7,8 +7,11 @@ import 'dart:async' as _i3; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' as _i2; +import 'package:image_picker_macos/src/messages.g.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; +import 'test_api.g.dart' as _i4; + // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters @@ -141,3 +144,64 @@ class MockFileSelectorPlatform extends _i1.Mock returnValue: _i3.Future>.value([]), ) as _i3.Future>); } + +/// A class which mocks [TestHostImagePickerApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestHostImagePickerApi extends _i1.Mock + implements _i4.TestHostImagePickerApi { + MockTestHostImagePickerApi() { + _i1.throwOnMissingStub(this); + } + + @override + bool supportsPHPicker() => (super.noSuchMethod( + Invocation.method( + #supportsPHPicker, + [], + ), + returnValue: false, + ) as bool); + + @override + _i3.Future> pickImages( + _i5.ImageSelectionOptions? options, + _i5.GeneralOptions? generalOptions, + ) => + (super.noSuchMethod( + Invocation.method( + #pickImages, + [ + options, + generalOptions, + ], + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); + + @override + _i3.Future> pickVideos(_i5.GeneralOptions? generalOptions) => + (super.noSuchMethod( + Invocation.method( + #pickVideos, + [generalOptions], + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); + + @override + _i3.Future> pickMedia( + _i5.MediaSelectionOptions? options, + _i5.GeneralOptions? generalOptions, + ) => + (super.noSuchMethod( + Invocation.method( + #pickMedia, + [ + options, + generalOptions, + ], + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); +} diff --git a/packages/image_picker/image_picker_macos/test/test_api.g.dart b/packages/image_picker/image_picker_macos/test/test_api.g.dart new file mode 100644 index 00000000000..e1f8eeba93d --- /dev/null +++ b/packages/image_picker/image_picker_macos/test/test_api.g.dart @@ -0,0 +1,174 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers +// ignore_for_file: avoid_relative_lib_imports +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:image_picker_macos/src/messages.g.dart'; + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is GeneralOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is MaxSize) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is ImageSelectionOptions) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is MediaSelectionOptions) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return GeneralOptions.decode(readValue(buffer)!); + case 130: + return MaxSize.decode(readValue(buffer)!); + case 131: + return ImageSelectionOptions.decode(readValue(buffer)!); + case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestHostImagePickerApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + bool supportsPHPicker(); + + Future> pickImages(ImageSelectionOptions options, GeneralOptions generalOptions); + + /// Currently, multi-video selection is unimplemented. + Future> pickVideos(GeneralOptions generalOptions); + + Future> pickMedia(MediaSelectionOptions options, GeneralOptions generalOptions); + + static void setUp(TestHostImagePickerApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, (Object? message) async { + try { + final bool output = api.supportsPHPicker(); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages was null.'); + final List args = (message as List?)!; + final ImageSelectionOptions? arg_options = (args[0] as ImageSelectionOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.'); + final GeneralOptions? arg_generalOptions = (args[1] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.'); + try { + final List output = await api.pickImages(arg_options!, arg_generalOptions!); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos was null.'); + final List args = (message as List?)!; + final GeneralOptions? arg_generalOptions = (args[0] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.'); + try { + final List output = await api.pickVideos(arg_generalOptions!); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia was null.'); + final List args = (message as List?)!; + final MediaSelectionOptions? arg_options = (args[0] as MediaSelectionOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); + final GeneralOptions? arg_generalOptions = (args[1] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.'); + try { + final List output = await api.pickMedia(arg_options!, arg_generalOptions!); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} From c98699f9ef3e81e50c6207e253edd23c5a534a6e Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 12:38:18 +0300 Subject: [PATCH 03/18] chore: generate pigeons and mocks with the latest dev dependencies Signed-off-by: Ellet --- .../image_picker/image_picker_macos/lib/src/messages.g.dart | 2 +- .../Sources/image_picker_macos/Messages.g.swift | 2 +- packages/image_picker/image_picker_macos/test/test_api.g.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/image_picker/image_picker_macos/lib/src/messages.g.dart b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart index 19d2bf53a06..86d1032011f 100644 --- a/packages/image_picker/image_picker_macos/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift index 1e0880d7299..646fbf647d3 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation diff --git a/packages/image_picker/image_picker_macos/test/test_api.g.dart b/packages/image_picker/image_picker_macos/test/test_api.g.dart index e1f8eeba93d..f9d1ad3a1db 100644 --- a/packages/image_picker/image_picker_macos/test/test_api.g.dart +++ b/packages/image_picker/image_picker_macos/test/test_api.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports From f420c91f48bef7a1735ad17029a7bf6f0deb584e Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 12:57:47 +0300 Subject: [PATCH 04/18] chore: format Swift generated file Messages.g.swift Signed-off-by: Ellet --- .../image_picker_macos/Messages.g.swift | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift index 646fbf647d3..f51231baef2 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift @@ -29,7 +29,7 @@ final class PigeonError: Error { var localizedDescription: String { return "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" - } + } } private func wrapResult(_ result: Any?) -> [Any?] { @@ -75,8 +75,6 @@ struct GeneralOptions { /// The value `0` means no limit. var limit: Int64 - - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> GeneralOptions? { let limit = pigeonVar_list[0] as! Int64 @@ -99,8 +97,6 @@ struct MaxSize { var width: Double? = nil var height: Double? = nil - - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> MaxSize? { let width: Double? = nilOrValue(pigeonVar_list[0]) @@ -130,8 +126,6 @@ struct ImageSelectionOptions { /// 100 indicates original quality. var quality: Int64 - - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> ImageSelectionOptions? { let maxSize: MaxSize? = nilOrValue(pigeonVar_list[0]) @@ -154,8 +148,6 @@ struct ImageSelectionOptions { struct MediaSelectionOptions { var imageSelectionOptions: ImageSelectionOptions - - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> MediaSelectionOptions? { let imageSelectionOptions = pigeonVar_list[0] as! ImageSelectionOptions @@ -222,23 +214,31 @@ class messagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { static let shared = messagesPigeonCodec(readerWriter: messagesPigeonCodecReaderWriter()) } - /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol ImagePickerApi { func supportsPHPicker() throws -> Bool - func pickImages(options: ImageSelectionOptions, generalOptions: GeneralOptions, completion: @escaping (Result<[String], Error>) -> Void) + func pickImages( + options: ImageSelectionOptions, generalOptions: GeneralOptions, + completion: @escaping (Result<[String], Error>) -> Void) /// Currently, multi-video selection is unimplemented. - func pickVideos(generalOptions: GeneralOptions, completion: @escaping (Result<[String], Error>) -> Void) - func pickMedia(options: MediaSelectionOptions, generalOptions: GeneralOptions, completion: @escaping (Result<[String], Error>) -> Void) + func pickVideos( + generalOptions: GeneralOptions, completion: @escaping (Result<[String], Error>) -> Void) + func pickMedia( + options: MediaSelectionOptions, generalOptions: GeneralOptions, + completion: @escaping (Result<[String], Error>) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class ImagePickerApiSetup { static var codec: FlutterStandardMessageCodec { messagesPigeonCodec.shared } /// Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ImagePickerApi?, messageChannelSuffix: String = "") { + static func setUp( + binaryMessenger: FlutterBinaryMessenger, api: ImagePickerApi?, messageChannelSuffix: String = "" + ) { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let supportsPHPickerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let supportsPHPickerChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { supportsPHPickerChannel.setMessageHandler { _, reply in do { @@ -251,7 +251,9 @@ class ImagePickerApiSetup { } else { supportsPHPickerChannel.setMessageHandler(nil) } - let pickImagesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let pickImagesChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { pickImagesChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -270,7 +272,9 @@ class ImagePickerApiSetup { pickImagesChannel.setMessageHandler(nil) } /// Currently, multi-video selection is unimplemented. - let pickVideosChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let pickVideosChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { pickVideosChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -287,7 +291,9 @@ class ImagePickerApiSetup { } else { pickVideosChannel.setMessageHandler(nil) } - let pickMediaChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let pickMediaChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) if let api = api { pickMediaChannel.setMessageHandler { message, reply in let args = message as! [Any?] From 3f499361ff3a85b7aedf1f4baf78321ed424f52f Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 13:00:01 +0300 Subject: [PATCH 05/18] chore: restore backticks of file_selector link Signed-off-by: Ellet --- packages/image_picker/image_picker_macos/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/image_picker/image_picker_macos/README.md b/packages/image_picker/image_picker_macos/README.md index d9da5ce328c..ac1bd85bc2c 100644 --- a/packages/image_picker/image_picker_macos/README.md +++ b/packages/image_picker/image_picker_macos/README.md @@ -26,7 +26,7 @@ they will see: `No photos` or `No Photos or Videos` message even if they have them as files on their desktop. The macOS Photos app supports importing images from an iOS device. > [!NOTE] -> This feature is only supported on **macOS 13.0 and newer versions**, on older versions it will fallback to using [file_selector][3] if enabled. +> This feature is only supported on **macOS 13.0 and newer versions**, on older versions it will fallback to using [`file_selector`][3] if enabled. > By defaults it's disabled on all versions. ## Limitations From 31c4f558fc5a677cdf76bf7498a0354af8219da4 Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 13:05:16 +0300 Subject: [PATCH 06/18] chore: update supported macOS version from 10.11 to 10.14 for image_picker_macos plugin Signed-off-by: Ellet --- .../image_picker_macos/macos/image_picker_macos.podspec | 2 +- .../image_picker_macos/macos/image_picker_macos/Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos.podspec b/packages/image_picker/image_picker_macos/macos/image_picker_macos.podspec index f493353d77f..9371c6dade3 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos.podspec +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos.podspec @@ -17,7 +17,7 @@ Downloaded by pub (not CocoaPods). s.source_files = 'image_picker_macos/Sources/image_picker_macos/**/*.swift' s.dependency 'FlutterMacOS' - s.platform = :osx, '10.11' + s.platform = :osx, '10.14' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Package.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Package.swift index 19042756f6c..2d0f14c22dc 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Package.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Package.swift @@ -10,7 +10,7 @@ import PackageDescription let package = Package( name: "image_picker_macos", platforms: [ - .macOS("10.11") + .macOS("10.14") ], products: [ .library(name: "image-picker-macos", targets: ["image_picker_macos"]) From abd926c5409232d08197cf741488834283cb7b43 Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 13:31:55 +0300 Subject: [PATCH 07/18] chore: use getVideo() instead of the soft-deprecated pickVideo() in unit test for consistency Signed-off-by: Ellet --- .../image_picker_macos/test/image_picker_macos_test.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart index 4cad84bb996..8d5bab85cbc 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -467,9 +467,8 @@ void main() { }); group('videos', () { - // TODO(EchoEllet): (Nit) Should this uses getVideo() instead of the soft-deprecated pickVideo() for consistency? - test('pickVideo passes the accepted type groups correctly', () async { - await plugin.pickVideo(source: ImageSource.gallery); + test('getVideo passes the accepted type groups correctly', () async { + await plugin.getVideo(source: ImageSource.gallery); final VerificationResult result = verify(mockFileSelectorPlatform .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); From 75badd9528ea0af067cf0e9f6223a0c5f7e4841e Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 13:41:17 +0300 Subject: [PATCH 08/18] chore: update outdated TODOs as they moved to https://github.com/flutter/packages/pull/8079 Signed-off-by: Ellet --- packages/image_picker/image_picker_macos/README.md | 3 +-- .../image_picker_macos/lib/image_picker_macos.dart | 2 +- .../Sources/image_picker_macos/ImagePickerImpl.swift | 2 +- .../image_picker_macos/test/image_picker_macos_test.dart | 1 - 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/image_picker/image_picker_macos/README.md b/packages/image_picker/image_picker_macos/README.md index ac1bd85bc2c..9faa8ca266d 100644 --- a/packages/image_picker/image_picker_macos/README.md +++ b/packages/image_picker/image_picker_macos/README.md @@ -34,8 +34,7 @@ have them as files on their desktop. The macOS Photos app supports importing ima `ImageSource.camera` is not supported unless a `cameraDelegate` is set. ### pickImage() - -The arguments `maxWidth`, `maxHeight`, `imageQuality` and `limit` are only supported when using the [PHPicker](#phpicker) implementation; they are not available in the default [file_selector][5] implementation. +The arguments `maxWidth`, `maxHeight`, `imageQuality`, and `limit` are only supported when using the [PHPicker](#phpicker) implementation; they are not available in the default [file_selector][5] implementation. The argument `requestFullMetadata` is unsupported on macOS. diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart index d509d019a3c..076629fdb61 100644 --- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -47,7 +47,7 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { /// Supports picking an image, multi-image, video, media, and multiple media. bool useMacOSPHPicker = false; - // TODO(EchoEllet): shouldUsePHPicker() and supportsPHPicker() should not be public, avoid using @visibleForTesting + // TODO(EchoEllet): avoid using @visibleForTesting per https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#avoid-using-visiblefortesting /// Return `true` if the current macOS version supports [useMacOSPHPicker]. /// diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift index f6cb5649cee..f33da3172b0 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift @@ -327,7 +327,7 @@ extension NSItemProvider { /// - Returns: The image file type (`png` or `jpeg`). func imageFileType(quality: Int64?) -> NSBitmapImageRep.FileType { let shouldCompress = shouldCompressImage(quality: quality) - // TODO(EchoEllet): The picked image can be JPEG even if it can represented as a PNG + // TODO(EchoEllet): The picked image can be JPEG even if it can represented as a PNG, should we always store as PNG in case quality is 100 but the image itself is JPEG or other type? return shouldCompress ? NSBitmapImageRep.FileType.jpeg : NSBitmapImageRep.FileType.png } diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart index 8d5bab85cbc..bfa54f6f46f 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -529,7 +529,6 @@ void main() { }, ); - // TODO(EchoEllet): Improve the test names for this and all related in this file test( 'getVideo uses file selector when PHPicker is disabled', () async { From 200cb107d72f8a723bfbe2cc66ee82fb692b94aa Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 13:51:55 +0300 Subject: [PATCH 09/18] test: add minor test for pickVideo Signed-off-by: Ellet --- .../image_picker_macos/test/image_picker_macos_test.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart index bfa54f6f46f..176a1883d6a 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -467,6 +467,13 @@ void main() { }); group('videos', () { + test('pickVideo delegate to getVideo', () async { + // The pickVideo is soft-deprecated in the platform interface + // and is only implemented for compatibility. Callers should be using getVideo. + await plugin.pickVideo(source: ImageSource.gallery); + verify(plugin.getVideo(source: ImageSource.gallery)).called(1); + }); + test('getVideo passes the accepted type groups correctly', () async { await plugin.getVideo(source: ImageSource.gallery); From d1bee0a00058aa26897398d2ad4cd9a7e7d2491a Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 14:00:16 +0300 Subject: [PATCH 10/18] chore: run dart format for Dart generated files Signed-off-by: Ellet --- .../lib/src/messages.g.dart | 68 +++++---- .../image_picker_macos/test/test_api.g.dart | 141 ++++++++++++------ 2 files changed, 132 insertions(+), 77 deletions(-) diff --git a/packages/image_picker/image_picker_macos/lib/src/messages.g.dart b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart index 86d1032011f..61d1f136eaf 100644 --- a/packages/image_picker/image_picker_macos/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart @@ -18,7 +18,8 @@ PlatformException _createConnectionError(String channelName) { ); } -List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { +List wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { if (empty) { return []; } @@ -131,7 +132,6 @@ class MediaSelectionOptions { } } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -139,16 +139,16 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is GeneralOptions) { + } else if (value is GeneralOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is MaxSize) { + } else if (value is MaxSize) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is ImageSelectionOptions) { + } else if (value is ImageSelectionOptions) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is MediaSelectionOptions) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); } else { @@ -159,13 +159,13 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return GeneralOptions.decode(readValue(buffer)!); - case 130: + case 130: return MaxSize.decode(readValue(buffer)!); - case 131: + case 131: return ImageSelectionOptions.decode(readValue(buffer)!); - case 132: + case 132: return MediaSelectionOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -177,9 +177,11 @@ class ImagePickerApi { /// Constructor for [ImagePickerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - ImagePickerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + ImagePickerApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -187,8 +189,10 @@ class ImagePickerApi { final String pigeonVar_messageChannelSuffix; Future supportsPHPicker() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, @@ -213,15 +217,18 @@ class ImagePickerApi { } } - Future> pickImages(ImageSelectionOptions options, GeneralOptions generalOptions) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + Future> pickImages( + ImageSelectionOptions options, GeneralOptions generalOptions) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([options, generalOptions]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([options, generalOptions]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -242,14 +249,16 @@ class ImagePickerApi { /// Currently, multi-video selection is unimplemented. Future> pickVideos(GeneralOptions generalOptions) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([generalOptions]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([generalOptions]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -268,15 +277,18 @@ class ImagePickerApi { } } - Future> pickMedia(MediaSelectionOptions options, GeneralOptions generalOptions) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + Future> pickMedia( + MediaSelectionOptions options, GeneralOptions generalOptions) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([options, generalOptions]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([options, generalOptions]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/packages/image_picker/image_picker_macos/test/test_api.g.dart b/packages/image_picker/image_picker_macos/test/test_api.g.dart index f9d1ad3a1db..d3451f0f609 100644 --- a/packages/image_picker/image_picker_macos/test/test_api.g.dart +++ b/packages/image_picker/image_picker_macos/test/test_api.g.dart @@ -13,7 +13,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_macos/src/messages.g.dart'; - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -21,16 +20,16 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is GeneralOptions) { + } else if (value is GeneralOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is MaxSize) { + } else if (value is MaxSize) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is ImageSelectionOptions) { + } else if (value is ImageSelectionOptions) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is MediaSelectionOptions) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); } else { @@ -41,13 +40,13 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return GeneralOptions.decode(readValue(buffer)!); - case 130: + case 130: return MaxSize.decode(readValue(buffer)!); - case 131: + case 131: return ImageSelectionOptions.decode(readValue(buffer)!); - case 132: + case 132: return MediaSelectionOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -56,116 +55,160 @@ class _PigeonCodec extends StandardMessageCodec { } abstract class TestHostImagePickerApi { - static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => TestDefaultBinaryMessengerBinding.instance; + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); bool supportsPHPicker(); - Future> pickImages(ImageSelectionOptions options, GeneralOptions generalOptions); + Future> pickImages( + ImageSelectionOptions options, GeneralOptions generalOptions); /// Currently, multi-video selection is unimplemented. Future> pickVideos(GeneralOptions generalOptions); - Future> pickMedia(MediaSelectionOptions options, GeneralOptions generalOptions); + Future> pickMedia( + MediaSelectionOptions options, GeneralOptions generalOptions); - static void setUp(TestHostImagePickerApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + TestHostImagePickerApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { - _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); } else { - _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, (Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { try { final bool output = api.supportsPHPicker(); return [output]; } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { - _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); } else { - _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, (Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages was null.'); + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages was null.'); final List args = (message as List?)!; - final ImageSelectionOptions? arg_options = (args[0] as ImageSelectionOptions?); + final ImageSelectionOptions? arg_options = + (args[0] as ImageSelectionOptions?); assert(arg_options != null, 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.'); - final GeneralOptions? arg_generalOptions = (args[1] as GeneralOptions?); + final GeneralOptions? arg_generalOptions = + (args[1] as GeneralOptions?); assert(arg_generalOptions != null, 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.'); try { - final List output = await api.pickImages(arg_options!, arg_generalOptions!); + final List output = + await api.pickImages(arg_options!, arg_generalOptions!); return [output]; } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { - _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); } else { - _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, (Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos was null.'); + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos was null.'); final List args = (message as List?)!; - final GeneralOptions? arg_generalOptions = (args[0] as GeneralOptions?); + final GeneralOptions? arg_generalOptions = + (args[0] as GeneralOptions?); assert(arg_generalOptions != null, 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.'); try { - final List output = await api.pickVideos(arg_generalOptions!); + final List output = + await api.pickVideos(arg_generalOptions!); return [output]; } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { - _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); } else { - _testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler(pigeonVar_channel, (Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia was null.'); + 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia was null.'); final List args = (message as List?)!; - final MediaSelectionOptions? arg_options = (args[0] as MediaSelectionOptions?); + final MediaSelectionOptions? arg_options = + (args[0] as MediaSelectionOptions?); assert(arg_options != null, 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); - final GeneralOptions? arg_generalOptions = (args[1] as GeneralOptions?); + final GeneralOptions? arg_generalOptions = + (args[1] as GeneralOptions?); assert(arg_generalOptions != null, 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.'); try { - final List output = await api.pickMedia(arg_options!, arg_generalOptions!); + final List output = + await api.pickMedia(arg_options!, arg_generalOptions!); return [output]; } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } From 30fd055c2955bbbaf504e66c18b433581cd6cb4c Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 14:11:28 +0300 Subject: [PATCH 11/18] chore: 'try' precede 'await' in ImagePickerImpl.swift Signed-off-by: Ellet --- .../Sources/image_picker_macos/ImagePickerImpl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift index f33da3172b0..729133881b7 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift @@ -472,7 +472,7 @@ class PickVideoHandler { let tempVideoUrl = FileManager.default.temporaryDirectory.appendingPathComponent( tempVideoFileName) - let videoData = await try itemProvider.loadDataRepresentation(for: videoType) + let videoData = try await itemProvider.loadDataRepresentation(for: videoType) try videoData.write(to: tempVideoUrl) let tempVideoPath = tempVideoUrl.pathString() From 5edff6472e2baa6a48f2282cf27ec0bfac1dc605 Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 14 Nov 2024 14:17:16 +0300 Subject: [PATCH 12/18] chore: hardcode the supportsPHPicker() name to fix CI warnings Signed-off-by: Ellet --- .../Sources/image_picker_macos/ImagePickerImpl.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift index 729133881b7..82055b65f52 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift @@ -37,7 +37,7 @@ class ImagePickerImpl: NSObject, ImagePickerApi { PigeonError( code: "UNSUPPORTED_PHPICKER", message: - "PHPicker is only supported on macOS 13.0 or newer. Use \(supportsPHPicker) to check.", + "PHPicker is only supported on macOS 13.0 or newer. Use `supportsPHPicker()` to check.", details: nil))) return } @@ -74,7 +74,7 @@ class ImagePickerImpl: NSObject, ImagePickerApi { PigeonError( code: "UNSUPPORTED_PHPICKER", message: - "PHPicker is only supported on macOS 13.0 or newer. Use \(supportsPHPicker) to check.", + "PHPicker is only supported on macOS 13.0 or newer. Use `supportsPHPicker()` to check.", details: nil))) return } @@ -117,7 +117,7 @@ class ImagePickerImpl: NSObject, ImagePickerApi { PigeonError( code: "UNSUPPORTED_PHPICKER", message: - "PHPicker is only supported on macOS 13.0 or newer. Use \(supportsPHPicker) to check.", + "PHPicker is only supported on macOS 13.0 or newer. Use `supportsPHPicker()` to check.", details: nil))) return } From 0aaf366aad8deaf281955760aa110dc08cdcce50 Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 2 Jan 2025 23:40:05 +0300 Subject: [PATCH 13/18] chore: remove @visibleForTesting from supportsPHPicker --- .../image_picker/image_picker_macos/lib/image_picker_macos.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart index 076629fdb61..ee545c9aa23 100644 --- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -53,7 +53,6 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { /// /// The [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller) /// is **supported on macOS 13.0+** - @visibleForTesting Future supportsPHPicker() => _hostApi.supportsPHPicker(); /// Returns `true` if [ImagePickerMacOS] should use [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller). From 527ac4873ba763d27164e4e452d9292b5baf628a Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 3 Jan 2025 00:30:52 +0300 Subject: [PATCH 14/18] chore: remove unnecessary assertions from NSImage.compressed --- .../image_picker_macos/lib/image_picker_macos.dart | 8 +++++++- .../Sources/image_picker_macos/ImageCompress.swift | 3 --- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart index ee545c9aa23..bb2bea30365 100644 --- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -246,8 +246,14 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { ImageSelectionOptions _imageOptionsToImageSelectionOptions( ImageOptions imageOptions, ) { + final int? imageQuality = imageOptions.imageQuality; + if (imageQuality != null && imageQuality < 0) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'quality cannot be negative'); + } + return ImageSelectionOptions( - quality: imageOptions.imageQuality ?? 100, + quality: imageQuality ?? 100, maxSize: MaxSize( width: imageOptions.maxWidth, height: imageOptions.maxHeight, diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift index 5a6dedd58da..c04f2e86249 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift @@ -19,9 +19,6 @@ extension NSImage { /// - Parameter quality: The quality of the image (0 to 100). /// - Returns: An optional `NSImage` that represents the compressed image. func compressed(quality: Int64) throws -> NSImage { - assert(quality != 100, "Quality 100 means no compression.") - assert(quality >= 0, "Quality can't be negative.") - guard let tiffData = self.tiffRepresentation, let bitmapRep = NSBitmapImageRep(data: tiffData) else { From b10b41f6dba69d85509b9fb6bb516a6294d6c035 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 3 Jan 2025 00:44:43 +0300 Subject: [PATCH 15/18] refactor: require quality as not nil in shouldCompressImage to avoid checks in compressedOrOriginal --- .../macos/RunnerTests/ImageCompressTests.swift | 2 -- .../Sources/image_picker_macos/ImageCompress.swift | 11 ++++------- .../Sources/image_picker_macos/ImagePickerImpl.swift | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift index a3fa3152dfd..ea251e95d8c 100644 --- a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift +++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift @@ -20,8 +20,6 @@ final class ImageCompressTests: XCTestCase { func testShouldCompressImage() { XCTAssertFalse(shouldCompressImage(quality: 100), "Quality 100 should not compress the image.") XCTAssertTrue(shouldCompressImage(quality: 80), "Quality bellow 100 should compress the image.") - XCTAssertFalse( - shouldCompressImage(quality: nil), "Should not compress the image when the quality is nil.") } func testImageCompression() throws { diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift index c04f2e86249..dcb18eb768a 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift @@ -9,8 +9,8 @@ import Foundation /// /// - Parameter quality: The quality level (0-100). A quality less than 100 indicates compression. /// - Returns: Whether the image should be compressed. -func shouldCompressImage(quality: Int64?) -> Bool { - return quality != nil && quality != 100 +func shouldCompressImage(quality: Int64) -> Bool { + return quality != 100 } extension NSImage { @@ -54,13 +54,10 @@ extension NSImage { /// If `nil` or if compression is not needed, the original image is returned. /// - Returns: The original or compressed `NSImage`. func compressedOrOriginal(quality: Int64?) throws -> NSImage { - if !shouldCompressImage(quality: quality) { + guard let quality = quality else { return self } - assert( - quality != nil, - "The quality expected to be not nil due to check using \(shouldCompressImage).") - guard let quality = quality else { + if !shouldCompressImage(quality: quality) { return self } return try compressed(quality: quality) diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift index 82055b65f52..c4931e87952 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift @@ -326,7 +326,7 @@ extension NSItemProvider { /// - Parameter quality: Determines if the image should be compressed based on the quality. /// - Returns: The image file type (`png` or `jpeg`). func imageFileType(quality: Int64?) -> NSBitmapImageRep.FileType { - let shouldCompress = shouldCompressImage(quality: quality) + let shouldCompress = quality != nil && shouldCompressImage(quality: quality!) // TODO(EchoEllet): The picked image can be JPEG even if it can represented as a PNG, should we always store as PNG in case quality is 100 but the image itself is JPEG or other type? return shouldCompress ? NSBitmapImageRep.FileType.jpeg : NSBitmapImageRep.FileType.png } From 0caa3904ab267ab78b5f57a400a48292e12766b5 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 3 Jan 2025 00:50:34 +0300 Subject: [PATCH 16/18] chore: remove unnecessary assertion from imageFileExt --- .../Sources/image_picker_macos/ImagePickerImpl.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift index c4931e87952..a4cc3d3b68d 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift @@ -336,10 +336,6 @@ func imageFileType(quality: Int64?) -> NSBitmapImageRep.FileType { /// - Parameter fileType: The image file type. /// - Returns: The image file extension. func imageFileExt(fileType: NSBitmapImageRep.FileType) -> String { - assert( - [NSBitmapImageRep.FileType.png, NSBitmapImageRep.FileType.jpeg].contains(fileType), - "Expected the image file type to be either PNG or JPEG." - ) switch fileType { case .jpeg: return "jpeg" case .png: return "png" From b6582fe99c6f72b8712f5f3eb80a15adc29e8940 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 3 Jan 2025 01:12:55 +0300 Subject: [PATCH 17/18] chore: extract duplicated createTestImage from ImageCompressTests and ImageResizeTests to ImageTestUtils --- .../macos/Runner.xcodeproj/project.pbxproj | 25 ++++++------------- .../RunnerTests/ImageCompressTests.swift | 9 ------- .../macos/RunnerTests/ImageResizeTests.swift | 9 ------- .../macos/RunnerTests/ImageTestUtils.swift | 14 +++++++++++ 4 files changed, 22 insertions(+), 35 deletions(-) create mode 100644 packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageTestUtils.swift diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj index 28d31a28a61..3548089a204 100644 --- a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXAggregateTarget section */ @@ -27,11 +27,12 @@ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - BBE2B8C47A32673657F9E2DC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B40E0F19CFF2111C7C9F07D /* Pods_Runner.framework */; }; + 7AA9FB252D27461200BBBB29 /* ImageTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA9FB242D27461000BBBB29 /* ImageTestUtils.swift */; }; 7ABC95832CBD9D810004CBA6 /* ImageCompressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */; }; 7ABC95842CBD9D810004CBA6 /* ImageResizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95802CBD9D810004CBA6 /* ImageResizeTests.swift */; }; 7ABC95852CBD9D810004CBA6 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95812CBD9D810004CBA6 /* RunnerTests.swift */; }; 7ABC95892CBD9D8A0004CBA6 /* RunnerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95862CBD9D8A0004CBA6 /* RunnerUITests.swift */; }; + BBE2B8C47A32673657F9E2DC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B40E0F19CFF2111C7C9F07D /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -72,8 +73,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 103E92CF1EBAAB82E57C06F1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 2E9F2DE12CD9DE067306B460 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -89,6 +88,7 @@ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 450A6A13EFEFF8F54A1685E1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7AA9FB242D27461000BBBB29 /* ImageTestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTestUtils.swift; sourceTree = ""; }; 7ABC952B2CB979800004CBA6 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7ABC95492CBAF9680004CBA6 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompressTests.swift; sourceTree = ""; }; @@ -129,16 +129,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0C0105042ACC016BCC44F609 /* Pods */ = { - isa = PBXGroup; - children = ( - 6D83DCCDFE2A45D91B8A2673 /* Pods-Runner.debug.xcconfig */, - 103E92CF1EBAAB82E57C06F1 /* Pods-Runner.release.xcconfig */, - CD373636C90085FB713CE436 /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( @@ -211,6 +201,7 @@ 7ABC95822CBD9D810004CBA6 /* RunnerTests */ = { isa = PBXGroup; children = ( + 7AA9FB242D27461000BBBB29 /* ImageTestUtils.swift */, 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */, 7ABC95802CBD9D810004CBA6 /* ImageResizeTests.swift */, 7ABC95812CBD9D810004CBA6 /* RunnerTests.swift */, @@ -241,7 +232,6 @@ EA63AF049335E53A5E609A89 /* Pods-Runner.release.xcconfig */, AFE3DE60B5E9D50D0C5C0524 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -353,7 +343,7 @@ ); mainGroup = 33CC10E42044A3C60003C045; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; @@ -474,6 +464,7 @@ 7ABC95832CBD9D810004CBA6 /* ImageCompressTests.swift in Sources */, 7ABC95842CBD9D810004CBA6 /* ImageResizeTests.swift in Sources */, 7ABC95852CBD9D810004CBA6 /* RunnerTests.swift in Sources */, + 7AA9FB252D27461200BBBB29 /* ImageTestUtils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -932,7 +923,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift index ea251e95d8c..e7b628afeb9 100644 --- a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift +++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift @@ -8,15 +8,6 @@ import XCTest final class ImageCompressTests: XCTestCase { - private func createTestImage(size: NSSize) -> NSImage { - let image = NSImage(size: size) - image.lockFocus() - NSColor.white.set() - NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill() - image.unlockFocus() - return image - } - func testShouldCompressImage() { XCTAssertFalse(shouldCompressImage(quality: 100), "Quality 100 should not compress the image.") XCTAssertTrue(shouldCompressImage(quality: 80), "Quality bellow 100 should compress the image.") diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift index a6a331d82eb..12aa1cda15e 100644 --- a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift +++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift @@ -8,15 +8,6 @@ import XCTest final class ImageResizeTests: XCTestCase { - private func createTestImage(size: NSSize) -> NSImage { - let image = NSImage(size: size) - image.lockFocus() - NSColor.black.set() - NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill() - image.unlockFocus() - return image - } - func testNilMaxSize() { let originalImage = createTestImage(size: NSSize(width: 1200, height: 800)) diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageTestUtils.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageTestUtils.swift new file mode 100644 index 00000000000..f37198205f7 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageTestUtils.swift @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AppKit + +func createTestImage(size: NSSize) -> NSImage { + let image = NSImage(size: size) + image.lockFocus() + NSColor.white.set() + NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill() + image.unlockFocus() + return image +} From c531757195d5ef069913332b7c50674b0e204743 Mon Sep 17 00:00:00 2001 From: Ellet Date: Sat, 4 Jan 2025 14:30:43 +0300 Subject: [PATCH 18/18] feat: improve error handling, avoid hardcoding string error codes, use pigeon for type-saftey --- .../integration_test/image_picker_test.dart | 16 -- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../lib/image_picker_macos.dart | 66 +++++++- .../lib/src/messages.g.dart | 137 ++++++++++++++-- .../image_picker_macos/ImageCompress.swift | 21 ++- .../image_picker_macos/ImagePickerImpl.swift | 154 +++++++----------- .../image_picker_macos/Messages.g.swift | 143 +++++++++++++--- .../image_picker_macos/pigeons/messages.dart | 68 +++++++- .../image_picker_macos/pubspec.yaml | 2 +- .../test/image_picker_macos_test.dart | 152 +++++++++++++++-- .../test/image_picker_macos_test.mocks.dart | 50 ++++-- .../image_picker_macos/test/test_api.g.dart | 46 ++++-- 12 files changed, 656 insertions(+), 201 deletions(-) diff --git a/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart b/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart index 921d3448aa1..d34915ca499 100644 --- a/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart +++ b/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart @@ -3,10 +3,8 @@ // found in the LICENSE file. import 'package:example/main.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_macos/image_picker_macos.dart'; -import 'package:image_picker_macos/src/messages.g.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; @@ -44,19 +42,5 @@ void main() { reason: 'Pressing the toggle button should update it correctly'); }, ); - testWidgets( - 'multi-video selection is not implemented', - (WidgetTester tester) async { - final ImagePickerApi hostApi = ImagePickerApi(); - await expectLater( - hostApi.pickVideos(GeneralOptions(limit: 2)), - throwsA(predicate( - (PlatformException e) => - e.code == 'UNIMPLEMENTED' && - e.message == 'Multi-video selection is not implemented', - )), - ); - }, - ); }); } diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj index 3548089a204..06bf3a683ab 100644 --- a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart index bb2bea30365..41951c7bfe0 100644 --- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -5,6 +5,7 @@ import 'package:file_selector_macos/file_selector_macos.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'src/messages.g.dart'; @@ -47,8 +48,6 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { /// Supports picking an image, multi-image, video, media, and multiple media. bool useMacOSPHPicker = false; - // TODO(EchoEllet): avoid using @visibleForTesting per https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#avoid-using-visiblefortesting - /// Return `true` if the current macOS version supports [useMacOSPHPicker]. /// /// The [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller) @@ -144,6 +143,8 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { _imageOptionsToImageSelectionOptions(options), GeneralOptions(limit: 1), )) + .getSuccessOrThrow() + .filePaths .firstOrNull; if (imagePath == null) { return null; @@ -183,7 +184,10 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { case ImageSource.gallery: if (await shouldUsePHPicker()) { final String? videoPath = - (await _hostApi.pickVideos(GeneralOptions(limit: 1))).firstOrNull; + (await _hostApi.pickVideos(GeneralOptions(limit: 1))) + .getSuccessOrThrow() + .filePaths + .firstOrNull; if (videoPath == null) { return null; } @@ -228,12 +232,14 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { MultiImagePickerOptions options = const MultiImagePickerOptions(), }) async { if (await shouldUsePHPicker()) { - final List images = await _hostApi.pickImages( + final List images = (await _hostApi.pickImages( _imageOptionsToImageSelectionOptions(options.imageOptions), GeneralOptions( limit: options.limit ?? 0, ), - ); + )) + .getSuccessOrThrow() + .filePaths; return images.map((String imagePath) => XFile(imagePath)).toList(); } const XTypeGroup typeGroup = @@ -267,7 +273,7 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { @override Future> getMedia({required MediaOptions options}) async { if (await shouldUsePHPicker()) { - final List images = await _hostApi.pickMedia( + final List images = (await _hostApi.pickMedia( MediaSelectionOptions( imageSelectionOptions: _imageOptionsToImageSelectionOptions(options.imageOptions), @@ -275,7 +281,9 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { GeneralOptions( limit: options.limit ?? (options.allowMultiple ? 0 : 1), ), - ); + )) + .getSuccessOrThrow() + .filePaths; return images.map((String mediaPath) => XFile(mediaPath)).toList(); } const XTypeGroup typeGroup = XTypeGroup( @@ -297,3 +305,47 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { return files; } } + +extension _ImagePickerResultExt on ImagePickerResult { + /// Returns the result as an [ImagePickerSuccessResult], or throws a [PlatformException] + /// if the result is an [ImagePickerErrorResult]. + ImagePickerSuccessResult getSuccessOrThrow() { + final ImagePickerResult result = this; + return switch (result) { + ImagePickerSuccessResult() => result, + ImagePickerErrorResult() => () { + final String errorMessage = switch (result.error) { + ImagePickerError.phpickerUnsupported => + 'PHPicker is only supported on macOS 13.0 or newer.', + ImagePickerError.windowNotFound => + 'No active window to present the picker.', + ImagePickerError.invalidImageSelection => + 'One of the selected items is not an image.', + ImagePickerError.invalidVideoSelection => + 'One of the selected items is not a video.', + ImagePickerError.imageLoadFailed => + 'An error occurred while loading the image.', + ImagePickerError.videoLoadFailed => + 'An error occurred while loading the video.', + ImagePickerError.imageConversionFailed => + 'Failed to convert the NSImage to TIFF data.', + ImagePickerError.imageSaveFailed => + 'Error saving the NSImage data to a file.', + ImagePickerError.imageCompressionFailed => + 'Error while compressing the Data of the NSImage.', + ImagePickerError.multiVideoSelectionUnsupported => + 'The multi-video selection is not supported.', + }; + // TODO(EchoEllet): Replace PlatformException with a plugin-specific exception. + // This is currently implemented to maintain compatibility with the existing behavior + // of other implementations of `image_picker`. For more details, refer to: + // https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling + throw PlatformException( + code: result.error.name, + message: errorMessage, + details: result.platformErrorMessage, + ); + }(), + }; + } +} diff --git a/packages/image_picker/image_picker_macos/lib/src/messages.g.dart b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart index 61d1f136eaf..87f38080f2f 100644 --- a/packages/image_picker/image_picker_macos/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -29,6 +29,45 @@ List wrapResponse( return [error.code, error.message, error.details]; } +/// Possible error conditions for [ImagePickerApi] calls. +enum ImagePickerError { + /// The current macOS version doesn't support [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller) + /// which is supported on macOS 13+. + phpickerUnsupported, + + /// Could not show the picker due to the missing window. + windowNotFound, + + /// When a `PHPickerResult` can't load `NSImage`. This error should not be reached + /// as the filter in the `PHPickerConfiguration` is set to accept only images. + invalidImageSelection, + + /// When a `PHPickerResult` is not a video. This error should not be reached + /// as the filter in the `PHPickerConfiguration` is set to accept only videos. + invalidVideoSelection, + + /// Could not load the image object as `NSImage`. + imageLoadFailed, + + /// Could not load the video data representation. + videoLoadFailed, + + /// The image tiff representation could not be loaded from the `NSImage`. + imageConversionFailed, + + /// The loaded `Data` from the `NSImage` could not be written as a file. + imageSaveFailed, + + /// The image could not be compressed or the `NSImage` could not be created + /// from the compressed `Data`. + imageCompressionFailed, + + /// The multi-video selection is not supported as it's not supported in + /// the app-facing package (`pickVideos` is missing). + /// The multi-video selection is supported when using `pickMedia` instead. + multiVideoSelectionUnsupported, +} + /// The common options between [ImageSelectionOptions], [VideoSelectionOptions] /// and [MediaSelectionOptions]. class GeneralOptions { @@ -132,6 +171,58 @@ class MediaSelectionOptions { } } +sealed class ImagePickerResult {} + +class ImagePickerSuccessResult extends ImagePickerResult { + ImagePickerSuccessResult({ + required this.filePaths, + }); + + /// The temporary file paths as a result of picking the images and/or videos. + List filePaths; + + Object encode() { + return [ + filePaths, + ]; + } + + static ImagePickerSuccessResult decode(Object result) { + result as List; + return ImagePickerSuccessResult( + filePaths: (result[0] as List?)!.cast(), + ); + } +} + +class ImagePickerErrorResult extends ImagePickerResult { + ImagePickerErrorResult({ + required this.error, + this.platformErrorMessage, + }); + + /// Potential error conditions for [ImagePickerApi] calls. + ImagePickerError error; + + /// Additional error message from the platform side. + String? platformErrorMessage; + + Object encode() { + return [ + error, + platformErrorMessage, + ]; + } + + static ImagePickerErrorResult decode(Object result) { + result as List; + return ImagePickerErrorResult( + error: result[0]! as ImagePickerError, + platformErrorMessage: result[1] as String?, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -139,17 +230,26 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is GeneralOptions) { + } else if (value is ImagePickerError) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is GeneralOptions) { + buffer.putUint8(130); writeValue(buffer, value.encode()); } else if (value is MaxSize) { - buffer.putUint8(130); + buffer.putUint8(131); writeValue(buffer, value.encode()); } else if (value is ImageSelectionOptions) { - buffer.putUint8(131); + buffer.putUint8(132); writeValue(buffer, value.encode()); } else if (value is MediaSelectionOptions) { - buffer.putUint8(132); + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is ImagePickerSuccessResult) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is ImagePickerErrorResult) { + buffer.putUint8(135); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -160,13 +260,20 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - return GeneralOptions.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : ImagePickerError.values[value]; case 130: - return MaxSize.decode(readValue(buffer)!); + return GeneralOptions.decode(readValue(buffer)!); case 131: - return ImageSelectionOptions.decode(readValue(buffer)!); + return MaxSize.decode(readValue(buffer)!); case 132: + return ImageSelectionOptions.decode(readValue(buffer)!); + case 133: return MediaSelectionOptions.decode(readValue(buffer)!); + case 134: + return ImagePickerSuccessResult.decode(readValue(buffer)!); + case 135: + return ImagePickerErrorResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -188,6 +295,8 @@ class ImagePickerApi { final String pigeonVar_messageChannelSuffix; + /// Returns whether [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller) + /// is supported on the current macOS version. Future supportsPHPicker() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker$pigeonVar_messageChannelSuffix'; @@ -217,7 +326,7 @@ class ImagePickerApi { } } - Future> pickImages( + Future pickImages( ImageSelectionOptions options, GeneralOptions generalOptions) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages$pigeonVar_messageChannelSuffix'; @@ -243,12 +352,12 @@ class ImagePickerApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)!.cast(); + return (pigeonVar_replyList[0] as ImagePickerResult?)!; } } /// Currently, multi-video selection is unimplemented. - Future> pickVideos(GeneralOptions generalOptions) async { + Future pickVideos(GeneralOptions generalOptions) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -273,11 +382,11 @@ class ImagePickerApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)!.cast(); + return (pigeonVar_replyList[0] as ImagePickerResult?)!; } } - Future> pickMedia( + Future pickMedia( MediaSelectionOptions options, GeneralOptions generalOptions) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia$pigeonVar_messageChannelSuffix'; @@ -303,7 +412,7 @@ class ImagePickerApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)!.cast(); + return (pigeonVar_replyList[0] as ImagePickerResult?)!; } } } diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift index dcb18eb768a..d4e2e76f079 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImageCompress.swift @@ -13,6 +13,15 @@ func shouldCompressImage(quality: Int64) -> Bool { return quality != 100 } +enum ImageCompressingError: Error { + /// Failed to convert the `NSImage` to TIFF data. + case conversionFailed + /// Failed to compress the image. + case compressionFailed + /// Failed to create `NSImage` from the compressed data. + case creationFailed +} + extension NSImage { /// Compresses the image to the specified quality. /// @@ -22,10 +31,7 @@ extension NSImage { guard let tiffData = self.tiffRepresentation, let bitmapRep = NSBitmapImageRep(data: tiffData) else { - // TODO(EchoEllet): Is there a convention for the error code? ImageConversionError or IMAGE_CONVERSION_ERROR or image-conversion-error. Update all codes. - throw PigeonError( - code: "ImageConversionError", message: "Failed to convert NSImage to TIFF data.", - details: nil) + throw ImageCompressingError.conversionFailed } // Convert quality from 0-100 to 0.0-1.0 @@ -35,14 +41,11 @@ extension NSImage { let compressedData = bitmapRep.representation( using: .jpeg, properties: [.compressionFactor: compressionQuality]) else { - throw PigeonError( - code: "CompressionError", message: "Failed to compress image.", details: nil) + throw ImageCompressingError.compressionFailed } guard let compressedImage = NSImage(data: compressedData) else { - throw PigeonError( - code: "ImageCreationError", message: "Failed to create NSImage from compressed data.", - details: nil) + throw ImageCompressingError.creationFailed } return compressedImage diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift index a4cc3d3b68d..0ec4ec70055 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/ImagePickerImpl.swift @@ -29,16 +29,10 @@ class ImagePickerImpl: NSObject, ImagePickerApi { func pickImages( options: ImageSelectionOptions, generalOptions: GeneralOptions, - completion: @escaping (Result<[String], any Error>) -> Void + completion: @escaping (Result) -> Void ) { guard #available(macOS 13.0, *) else { - completion( - .failure( - PigeonError( - code: "UNSUPPORTED_PHPICKER", - message: - "PHPicker is only supported on macOS 13.0 or newer. Use `supportsPHPicker()` to check.", - details: nil))) + completion(.success(ImagePickerErrorResult(error: .phpickerUnsupported))) return } @@ -57,34 +51,21 @@ class ImagePickerImpl: NSObject, ImagePickerApi { showPHPicker( picker, noActiveWindow: { - completion( - .failure( - PigeonError( - code: "WINDOW_NOT_FOUND", message: "No active window to present the picker.", - details: nil))) + completion(.success(ImagePickerErrorResult(error: .windowNotFound))) }) } func pickVideos( - generalOptions: GeneralOptions, completion: @escaping (Result<[String], any Error>) -> Void + generalOptions: GeneralOptions, + completion: @escaping (Result) -> Void ) { guard #available(macOS 13.0, *) else { - completion( - .failure( - PigeonError( - code: "UNSUPPORTED_PHPICKER", - message: - "PHPicker is only supported on macOS 13.0 or newer. Use `supportsPHPicker()` to check.", - details: nil))) + completion(.success(ImagePickerErrorResult(error: .phpickerUnsupported))) return } if generalOptions.limit != nil && generalOptions.limit != 1 { - completion( - .failure( - PigeonError( - code: "UNIMPLEMENTED", message: "Multi-video selection is not implemented", details: nil - ))) + completion(.success(ImagePickerErrorResult(error: .multiVideoSelectionUnsupported))) return } @@ -99,26 +80,16 @@ class ImagePickerImpl: NSObject, ImagePickerApi { showPHPicker( picker, noActiveWindow: { - completion( - .failure( - PigeonError( - code: "WINDOW_NOT_FOUND", message: "No active window to present the picker.", - details: nil))) + completion(.success(ImagePickerErrorResult(error: .windowNotFound))) }) } func pickMedia( options: MediaSelectionOptions, generalOptions: GeneralOptions, - completion: @escaping (Result<[String], any Error>) -> Void + completion: @escaping (Result) -> Void ) { guard #available(macOS 13.0, *) else { - completion( - .failure( - PigeonError( - code: "UNSUPPORTED_PHPICKER", - message: - "PHPicker is only supported on macOS 13.0 or newer. Use `supportsPHPicker()` to check.", - details: nil))) + completion(.success(ImagePickerErrorResult(error: .phpickerUnsupported))) return } @@ -133,11 +104,7 @@ class ImagePickerImpl: NSObject, ImagePickerApi { showPHPicker( picker, noActiveWindow: { - completion( - .failure( - PigeonError( - code: "WINDOW_NOT_FOUND", message: "No active window to present the picker.", - details: nil))) + completion(.success(ImagePickerErrorResult(error: .windowNotFound))) }) } @@ -154,11 +121,12 @@ class ImagePickerImpl: NSObject, ImagePickerApi { } class PickImagesDelegate: PHPickerViewControllerDelegate { - private let completion: ((Result<[String], any Error>) -> Void) + private let completion: ((Result) -> Void) private let options: ImageSelectionOptions init( - completion: @escaping ((Result<[String], any Error>) -> Void), options: ImageSelectionOptions + completion: @escaping ((Result) -> Void), + options: ImageSelectionOptions ) { self.completion = completion self.options = options @@ -169,7 +137,7 @@ class PickImagesDelegate: PHPickerViewControllerDelegate { picker.dismiss(nil) if results.isEmpty { - completion(.success([])) + completion(.success(ImagePickerSuccessResult(filePaths: []))) return } @@ -179,11 +147,7 @@ class PickImagesDelegate: PHPickerViewControllerDelegate { for result in results { let itemProvider = result.itemProvider guard itemProvider.canLoadObject(ofClass: NSImage.self) else { - completion( - .failure( - PigeonError( - code: "INVALID_SELECTION", message: "One of the selected items is not an image", - details: nil))) + completion(.success(ImagePickerErrorResult(error: .invalidImageSelection))) return } @@ -194,16 +158,16 @@ class PickImagesDelegate: PHPickerViewControllerDelegate { else { return } savedFilePaths.append(tempImagePath) } - completion(.success(savedFilePaths)) + completion(.success(ImagePickerSuccessResult(filePaths: savedFilePaths))) } } } // Currently, multi-video selection is unimplemented. class PickVideosDelegate: PHPickerViewControllerDelegate { - private let completion: ((Result<[String], any Error>) -> Void) + private let completion: ((Result) -> Void) - init(completion: @escaping ((Result<[String], any Error>) -> Void)) { + init(completion: @escaping ((Result) -> Void)) { self.completion = completion } @@ -212,16 +176,13 @@ class PickVideosDelegate: PHPickerViewControllerDelegate { picker.dismiss(nil) guard let itemProvider = results.first?.itemProvider else { - completion(.success([])) + completion(.success(ImagePickerSuccessResult(filePaths: []))) return } let canLoadVideo = itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) if !canLoadVideo { - completion( - .failure( - PigeonError( - code: "INVALID_SELECTION", message: "The selected item is not a video", details: nil))) + completion(.success(ImagePickerErrorResult(error: .invalidVideoSelection))) return } @@ -231,18 +192,20 @@ class PickVideosDelegate: PHPickerViewControllerDelegate { .processAndSave(itemProvider: itemProvider) else { return } - completion(.success([tempVideoPath])) + completion(.success(ImagePickerSuccessResult(filePaths: [tempVideoPath]))) } } } class PickMediaDelegate: PHPickerViewControllerDelegate { - private let completion: ((Result<[String], any Error>) -> Void) + private let completion: ((Result) -> Void) private let options: MediaSelectionOptions - init(completion: @escaping (Result<[String], any Error>) -> Void, options: MediaSelectionOptions) - { + init( + completion: @escaping (Result) -> Void, + options: MediaSelectionOptions + ) { self.completion = completion self.options = options } @@ -252,7 +215,7 @@ class PickMediaDelegate: PHPickerViewControllerDelegate { picker.dismiss(nil) if results.isEmpty { - completion(.success([])) + completion(.success(ImagePickerSuccessResult(filePaths: []))) return } @@ -282,7 +245,7 @@ class PickMediaDelegate: PHPickerViewControllerDelegate { } } - completion(.success(savedFilePaths)) + completion(.success(ImagePickerSuccessResult(filePaths: savedFilePaths))) } } @@ -370,11 +333,13 @@ func generateTempImageFilePath(imageFileType: NSBitmapImageRep.FileType) -> URL /// Shared image handling between `PickImageDelegate` and `PickMediaDelegate`. class PickImageHandler { - let completion: ((Result<[String], any Error>) -> Void) + let completion: ((Result) -> Void) let options: ImageSelectionOptions - init(completion: @escaping (Result<[String], any Error>) -> Void, options: ImageSelectionOptions) - { + init( + completion: @escaping (Result) -> Void, + options: ImageSelectionOptions + ) { self.completion = completion self.options = options } @@ -391,10 +356,9 @@ class PickImageHandler { return tempImagePath } catch { completion( - .failure( - PigeonError( - code: "IMAGE_LOAD_FAILED", - message: "Error loading image: \(error.localizedDescription)", details: nil))) + .success( + ImagePickerErrorResult( + error: .imageLoadFailed, platformErrorMessage: error.localizedDescription))) return nil } } @@ -409,11 +373,7 @@ class PickImageHandler { let bitmapRep = NSBitmapImageRep(data: tiffData), let imageData = bitmapRep.representation(using: imageFileType, properties: [:]) else { - completion( - .failure( - PigeonError( - code: "IMAGE_CONVERSION_FAILED", message: "Failed to convert NSImage to TIFF data.", - details: nil))) + completion(.success(ImagePickerErrorResult(error: .imageConversionFailed))) return nil } @@ -424,10 +384,9 @@ class PickImageHandler { return filePath.pathString() } catch { completion( - .failure( - PigeonError( - code: "IMAGE_SAVE_FAILED", message: "Error saving image to file: \(error)", details: nil - ))) + .success( + ImagePickerErrorResult( + error: .imageSaveFailed, platformErrorMessage: error.localizedDescription))) return nil } } @@ -437,15 +396,21 @@ class PickImageHandler { /// Returns `nil` if an error occurs, and handles. private func processImage(_ image: NSImage) -> NSImage? { do { - let processedImage = try image.resizedOrOriginal(maxSize: options.maxSize) - .compressedOrOriginal(quality: options.quality) - return processedImage + let resizedOrOriginalImage = image.resizedOrOriginal(maxSize: options.maxSize) + let compressedOrOriginalImage = try resizedOrOriginalImage.compressedOrOriginal( + quality: options.quality) + return compressedOrOriginalImage + } catch ImageCompressingError.conversionFailed { + completion( + .success( + ImagePickerErrorResult( + error: .imageConversionFailed))) + return nil } catch { completion( - .failure( - PigeonError( - code: "IMAGE_PROCESSING_FAILED", - message: "Error processing image: \(error.localizedDescription)", details: nil))) + .success( + ImagePickerErrorResult( + error: .imageCompressionFailed, platformErrorMessage: error.localizedDescription))) return nil } } @@ -453,9 +418,9 @@ class PickImageHandler { /// Shared image handling between `PickVideosDelegate` and `PickMediaDelegate`. class PickVideoHandler { - let completion: ((Result<[String], any Error>) -> Void) + let completion: ((Result) -> Void) - init(completion: @escaping (Result<[String], any Error>) -> Void) { + init(completion: @escaping (Result) -> Void) { self.completion = completion } @@ -476,10 +441,9 @@ class PickVideoHandler { return tempVideoPath } catch { completion( - .failure( - PigeonError( - code: "VIDEO_LOAD_FAILED", - message: "Error loading a video: \(error.localizedDescription)", details: nil))) + .success( + ImagePickerErrorResult( + error: .videoLoadFailed, platformErrorMessage: error.localizedDescription))) return nil } } diff --git a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift index f51231baef2..6dc2e0f9aaf 100644 --- a/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift +++ b/packages/image_picker/image_picker_macos/macos/image_picker_macos/Sources/image_picker_macos/Messages.g.swift @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -67,6 +67,36 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } +/// Possible error conditions for [ImagePickerApi] calls. +enum ImagePickerError: Int { + /// The current macOS version doesn't support [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller) + /// which is supported on macOS 13+. + case phpickerUnsupported = 0 + /// Could not show the picker due to the missing window. + case windowNotFound = 1 + /// When a `PHPickerResult` can't load `NSImage`. This error should not be reached + /// as the filter in the `PHPickerConfiguration` is set to accept only images. + case invalidImageSelection = 2 + /// When a `PHPickerResult` is not a video. This error should not be reached + /// as the filter in the `PHPickerConfiguration` is set to accept only videos. + case invalidVideoSelection = 3 + /// Could not load the image object as `NSImage`. + case imageLoadFailed = 4 + /// Could not load the video data representation. + case videoLoadFailed = 5 + /// The image tiff representation could not be loaded from the `NSImage`. + case imageConversionFailed = 6 + /// The loaded `Data` from the `NSImage` could not be written as a file. + case imageSaveFailed = 7 + /// The image could not be compressed or the `NSImage` could not be created + /// from the compressed `Data`. + case imageCompressionFailed = 8 + /// The multi-video selection is not supported as it's not supported in + /// the app-facing package (`pickVideos` is missing). + /// The multi-video selection is supported when using `pickMedia` instead. + case multiVideoSelectionUnsupported = 9 +} + /// The common options between [ImageSelectionOptions], [VideoSelectionOptions] /// and [MediaSelectionOptions]. /// @@ -163,36 +193,106 @@ struct MediaSelectionOptions { } } -private class messagesPigeonCodecReader: FlutterStandardReader { +/// Generated class from Pigeon that represents data sent in messages. +/// This protocol should not be extended by any user class outside of the generated file. +protocol ImagePickerResult { + +} + +/// Generated class from Pigeon that represents data sent in messages. +struct ImagePickerSuccessResult: ImagePickerResult { + /// The temporary file paths as a result of picking the images and/or videos. + var filePaths: [String] + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ImagePickerSuccessResult? { + let filePaths = pigeonVar_list[0] as! [String] + + return ImagePickerSuccessResult( + filePaths: filePaths + ) + } + func toList() -> [Any?] { + return [ + filePaths + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct ImagePickerErrorResult: ImagePickerResult { + /// Potential error conditions for [ImagePickerApi] calls. + var error: ImagePickerError + /// Additional error message from the platform side. + var platformErrorMessage: String? = nil + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ImagePickerErrorResult? { + let error = pigeonVar_list[0] as! ImagePickerError + let platformErrorMessage: String? = nilOrValue(pigeonVar_list[1]) + + return ImagePickerErrorResult( + error: error, + platformErrorMessage: platformErrorMessage + ) + } + func toList() -> [Any?] { + return [ + error, + platformErrorMessage, + ] + } +} + +private class MessagesPigeonCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 129: - return GeneralOptions.fromList(self.readValue() as! [Any?]) + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return ImagePickerError(rawValue: enumResultAsInt) + } + return nil case 130: - return MaxSize.fromList(self.readValue() as! [Any?]) + return GeneralOptions.fromList(self.readValue() as! [Any?]) case 131: - return ImageSelectionOptions.fromList(self.readValue() as! [Any?]) + return MaxSize.fromList(self.readValue() as! [Any?]) case 132: + return ImageSelectionOptions.fromList(self.readValue() as! [Any?]) + case 133: return MediaSelectionOptions.fromList(self.readValue() as! [Any?]) + case 134: + return ImagePickerSuccessResult.fromList(self.readValue() as! [Any?]) + case 135: + return ImagePickerErrorResult.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } } } -private class messagesPigeonCodecWriter: FlutterStandardWriter { +private class MessagesPigeonCodecWriter: FlutterStandardWriter { override func writeValue(_ value: Any) { - if let value = value as? GeneralOptions { + if let value = value as? ImagePickerError { super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? GeneralOptions { + super.writeByte(130) super.writeValue(value.toList()) } else if let value = value as? MaxSize { - super.writeByte(130) + super.writeByte(131) super.writeValue(value.toList()) } else if let value = value as? ImageSelectionOptions { - super.writeByte(131) + super.writeByte(132) super.writeValue(value.toList()) } else if let value = value as? MediaSelectionOptions { - super.writeByte(132) + super.writeByte(133) + super.writeValue(value.toList()) + } else if let value = value as? ImagePickerSuccessResult { + super.writeByte(134) + super.writeValue(value.toList()) + } else if let value = value as? ImagePickerErrorResult { + super.writeByte(135) super.writeValue(value.toList()) } else { super.writeValue(value) @@ -200,42 +300,47 @@ private class messagesPigeonCodecWriter: FlutterStandardWriter { } } -private class messagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { +private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { override func reader(with data: Data) -> FlutterStandardReader { - return messagesPigeonCodecReader(data: data) + return MessagesPigeonCodecReader(data: data) } override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return messagesPigeonCodecWriter(data: data) + return MessagesPigeonCodecWriter(data: data) } } -class messagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = messagesPigeonCodec(readerWriter: messagesPigeonCodecReaderWriter()) +class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol ImagePickerApi { + /// Returns whether [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller) + /// is supported on the current macOS version. func supportsPHPicker() throws -> Bool func pickImages( options: ImageSelectionOptions, generalOptions: GeneralOptions, - completion: @escaping (Result<[String], Error>) -> Void) + completion: @escaping (Result) -> Void) /// Currently, multi-video selection is unimplemented. func pickVideos( - generalOptions: GeneralOptions, completion: @escaping (Result<[String], Error>) -> Void) + generalOptions: GeneralOptions, completion: @escaping (Result) -> Void + ) func pickMedia( options: MediaSelectionOptions, generalOptions: GeneralOptions, - completion: @escaping (Result<[String], Error>) -> Void) + completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class ImagePickerApiSetup { - static var codec: FlutterStandardMessageCodec { messagesPigeonCodec.shared } + static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } /// Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. static func setUp( binaryMessenger: FlutterBinaryMessenger, api: ImagePickerApi?, messageChannelSuffix: String = "" ) { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + /// Returns whether [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller) + /// is supported on the current macOS version. let supportsPHPickerChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) diff --git a/packages/image_picker/image_picker_macos/pigeons/messages.dart b/packages/image_picker/image_picker_macos/pigeons/messages.dart index d8fc53a6c01..d4731885301 100644 --- a/packages/image_picker/image_picker_macos/pigeons/messages.dart +++ b/packages/image_picker/image_picker_macos/pigeons/messages.dart @@ -56,29 +56,87 @@ class MediaSelectionOptions { ImageSelectionOptions imageSelectionOptions; } +/// Possible error conditions for [ImagePickerApi] calls. +enum ImagePickerError { + /// The current macOS version doesn't support [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller) + /// which is supported on macOS 13+. + phpickerUnsupported, + + /// Could not show the picker due to the missing window. + windowNotFound, + + /// When a `PHPickerResult` can't load `NSImage`. This error should not be reached + /// as the filter in the `PHPickerConfiguration` is set to accept only images. + invalidImageSelection, + + /// When a `PHPickerResult` is not a video. This error should not be reached + /// as the filter in the `PHPickerConfiguration` is set to accept only videos. + invalidVideoSelection, + + /// Could not load the image object as `NSImage`. + imageLoadFailed, + + /// Could not load the video data representation. + videoLoadFailed, + + /// The image tiff representation could not be loaded from the `NSImage`. + imageConversionFailed, + + /// The loaded `Data` from the `NSImage` could not be written as a file. + imageSaveFailed, + + /// The image could not be compressed or the `NSImage` could not be created + /// from the compressed `Data`. + imageCompressionFailed, + + /// The multi-video selection is not supported as it's not supported in + /// the app-facing package (`pickVideos` is missing). + /// The multi-video selection is supported when using `pickMedia` instead. + multiVideoSelectionUnsupported; +} + +sealed class ImagePickerResult {} + +class ImagePickerSuccessResult extends ImagePickerResult { + ImagePickerSuccessResult(this.filePaths); + + /// The temporary file paths as a result of picking the images and/or videos. + final List filePaths; +} + +class ImagePickerErrorResult extends ImagePickerResult { + ImagePickerErrorResult(this.error, {this.platformErrorMessage}); + + /// Potential error conditions for [ImagePickerApi] calls. + final ImagePickerError error; + + /// Additional error message from the platform side. + final String? platformErrorMessage; +} + @HostApi(dartHostTestHandler: 'TestHostImagePickerApi') abstract class ImagePickerApi { + /// Returns whether [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller) + /// is supported on the current macOS version. bool supportsPHPicker(); // TODO(EchoEllet): Should ImagePickerApi be more similar to image_picker_ios or image_picker_android messages.dart? // `pickImage()` and `pickMultiImage()` vs `pickImages()` with `limit` and `allowMultiple`. // Currently it's closer to the image_picker_android messages.dart but without allowMultiple - // Return file paths - @async - List pickImages( + ImagePickerResult pickImages( ImageSelectionOptions options, GeneralOptions generalOptions, ); /// Currently, multi-video selection is unimplemented. @async - List pickVideos( + ImagePickerResult pickVideos( GeneralOptions generalOptions, ); @async - List pickMedia( + ImagePickerResult pickMedia( MediaSelectionOptions options, GeneralOptions generalOptions, ); diff --git a/packages/image_picker/image_picker_macos/pubspec.yaml b/packages/image_picker/image_picker_macos/pubspec.yaml index 01a70774726..269c00c7e10 100644 --- a/packages/image_picker/image_picker_macos/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/pubspec.yaml @@ -28,7 +28,7 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.4.4 - pigeon: ^22.6.1 + pigeon: ^22.7.2 topics: - image-picker diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart index 176a1883d6a..ae036c87cb8 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_macos/image_picker_macos.dart'; import 'package:image_picker_macos/src/messages.g.dart'; @@ -42,19 +43,25 @@ void main() { when(mockImagePickerApi.supportsPHPicker()).thenAnswer((_) => false); - when(mockImagePickerApi.pickImages(any, any)) - .thenAnswer((_) async => []); + when(mockImagePickerApi.pickImages(any, any)).thenAnswer( + (_) async => ImagePickerSuccessResult(filePaths: [])); - when(mockImagePickerApi.pickVideos(any)) - .thenAnswer((_) async => []); + when(mockImagePickerApi.pickVideos(any)).thenAnswer( + (_) async => ImagePickerSuccessResult(filePaths: [])); - when(mockImagePickerApi.pickMedia(any, any)) - .thenAnswer((_) async => []); + when(mockImagePickerApi.pickMedia(any, any)).thenAnswer( + (_) async => ImagePickerSuccessResult(filePaths: [])); ImagePickerMacOS.fileSelector = mockFileSelectorPlatform; TestHostImagePickerApi.setUp(mockImagePickerApi); }); + setUpAll(() { + // Mockito cannot generate a dummy value of type ImagePickerResult + provideDummy( + ImagePickerSuccessResult(filePaths: [])); + }); + void testWithPHPicker({ required bool enabled, required void Function() body, @@ -364,7 +371,7 @@ void main() { any, any, )).thenAnswer((_) async { - return filePaths; + return ImagePickerSuccessResult(filePaths: filePaths); }); expect( (await plugin.pickImage(source: ImageSource.gallery))?.path, @@ -390,7 +397,7 @@ void main() { any, any, )).thenAnswer((_) async { - return filePaths; + return ImagePickerSuccessResult(filePaths: filePaths); }); expect( (await plugin.getMultiImageWithOptions()) @@ -464,6 +471,57 @@ void main() { ); }, ); + + void testThrowsPlatformExceptionForPHPicker({ + required String methodName, + required Future Function() underTest, + }) => + test( + '$methodName throws $PlatformException for PHPicker on platform API error', + () { + testWithPHPicker( + enabled: true, + body: () async { + for (final ImagePickerError error + in ImagePickerError.values) { + const String platformErrorMessage = + 'Example Platform Error Message'; + when(mockImagePickerApi.pickImages(any, any)) + .thenAnswer((_) async => ImagePickerErrorResult( + error: error, + platformErrorMessage: platformErrorMessage, + )); + await expectLater( + () => underTest(), + throwsA( + isA() + .having( + (PlatformException e) => e.code, + 'code', + equals(error.name), + ) + .having( + (PlatformException e) => e.details, + 'details', + equals(platformErrorMessage), + ), + ), + ); + verify(mockImagePickerApi.pickImages(any, any)).called(1); + } + }); + }, + ); + + testThrowsPlatformExceptionForPHPicker( + methodName: 'getImageFromSource', + underTest: () => plugin.getImageFromSource(source: ImageSource.gallery), + ); + + testThrowsPlatformExceptionForPHPicker( + methodName: 'getMultiImageWithOptions', + underTest: () => plugin.getMultiImageWithOptions(), + ); }); group('videos', () { @@ -583,7 +641,7 @@ void main() { when(mockImagePickerApi.pickVideos( any, )).thenAnswer((_) async { - return filePaths; + return ImagePickerSuccessResult(filePaths: filePaths); }); expect( (await plugin.getVideo(source: ImageSource.gallery))?.path, @@ -593,6 +651,42 @@ void main() { ); }, ); + + test( + 'getVideo throws $PlatformException for PHPicker on platform API error', + () { + testWithPHPicker( + enabled: true, + body: () async { + for (final ImagePickerError error in ImagePickerError.values) { + const String platformErrorMessage = + 'Example Platform Error Message'; + when(mockImagePickerApi.pickVideos(any)) + .thenAnswer((_) async => ImagePickerErrorResult( + error: error, + platformErrorMessage: platformErrorMessage, + )); + await expectLater( + () => plugin.getVideo(source: ImageSource.gallery), + throwsA( + isA() + .having( + (PlatformException e) => e.code, + 'code', + equals(error.name), + ) + .having( + (PlatformException e) => e.details, + 'details', + equals(platformErrorMessage), + ), + ), + ); + verify(mockImagePickerApi.pickVideos(any)).called(1); + } + }); + }, + ); }); group('media', () { @@ -731,7 +825,7 @@ void main() { any, any, )).thenAnswer((_) async { - return filePaths; + return ImagePickerSuccessResult(filePaths: filePaths); }); expect( (await plugin.getMedia( @@ -805,6 +899,44 @@ void main() { ); }, ); + + test( + 'getMedia throws $PlatformException for PHPicker on platform API error', + () { + testWithPHPicker( + enabled: true, + body: () async { + for (final ImagePickerError error in ImagePickerError.values) { + const String platformErrorMessage = + 'Example Platform Error Message'; + when(mockImagePickerApi.pickMedia(any, any)) + .thenAnswer((_) async => ImagePickerErrorResult( + error: error, + platformErrorMessage: platformErrorMessage, + )); + await expectLater( + () => plugin.getMedia( + options: const MediaOptions(allowMultiple: true), + ), + throwsA( + isA() + .having( + (PlatformException e) => e.code, + 'code', + equals(error.name), + ) + .having( + (PlatformException e) => e.details, + 'details', + equals(platformErrorMessage), + ), + ), + ); + verify(mockImagePickerApi.pickMedia(any, any)).called(1); + } + }); + }, + ); }); } diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart index 71019b86c8c..1e9d0865a1c 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.5 from annotations // in image_picker_macos/test/image_picker_macos_test.dart. // Do not manually edit this file. @@ -9,6 +9,7 @@ import 'package:file_selector_platform_interface/file_selector_platform_interfac as _i2; import 'package:image_picker_macos/src/messages.g.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i6; import 'test_api.g.dart' as _i4; @@ -20,6 +21,7 @@ import 'test_api.g.dart' as _i4; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types @@ -164,7 +166,7 @@ class MockTestHostImagePickerApi extends _i1.Mock ) as bool); @override - _i3.Future> pickImages( + _i3.Future<_i5.ImagePickerResult> pickImages( _i5.ImageSelectionOptions? options, _i5.GeneralOptions? generalOptions, ) => @@ -176,21 +178,39 @@ class MockTestHostImagePickerApi extends _i1.Mock generalOptions, ], ), - returnValue: _i3.Future>.value([]), - ) as _i3.Future>); + returnValue: _i3.Future<_i5.ImagePickerResult>.value( + _i6.dummyValue<_i5.ImagePickerResult>( + this, + Invocation.method( + #pickImages, + [ + options, + generalOptions, + ], + ), + )), + ) as _i3.Future<_i5.ImagePickerResult>); @override - _i3.Future> pickVideos(_i5.GeneralOptions? generalOptions) => + _i3.Future<_i5.ImagePickerResult> pickVideos( + _i5.GeneralOptions? generalOptions) => (super.noSuchMethod( Invocation.method( #pickVideos, [generalOptions], ), - returnValue: _i3.Future>.value([]), - ) as _i3.Future>); + returnValue: _i3.Future<_i5.ImagePickerResult>.value( + _i6.dummyValue<_i5.ImagePickerResult>( + this, + Invocation.method( + #pickVideos, + [generalOptions], + ), + )), + ) as _i3.Future<_i5.ImagePickerResult>); @override - _i3.Future> pickMedia( + _i3.Future<_i5.ImagePickerResult> pickMedia( _i5.MediaSelectionOptions? options, _i5.GeneralOptions? generalOptions, ) => @@ -202,6 +222,16 @@ class MockTestHostImagePickerApi extends _i1.Mock generalOptions, ], ), - returnValue: _i3.Future>.value([]), - ) as _i3.Future>); + returnValue: _i3.Future<_i5.ImagePickerResult>.value( + _i6.dummyValue<_i5.ImagePickerResult>( + this, + Invocation.method( + #pickMedia, + [ + options, + generalOptions, + ], + ), + )), + ) as _i3.Future<_i5.ImagePickerResult>); } diff --git a/packages/image_picker/image_picker_macos/test/test_api.g.dart b/packages/image_picker/image_picker_macos/test/test_api.g.dart index d3451f0f609..2116f7e7d24 100644 --- a/packages/image_picker/image_picker_macos/test/test_api.g.dart +++ b/packages/image_picker/image_picker_macos/test/test_api.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports @@ -20,17 +20,26 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is GeneralOptions) { + } else if (value is ImagePickerError) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is GeneralOptions) { + buffer.putUint8(130); writeValue(buffer, value.encode()); } else if (value is MaxSize) { - buffer.putUint8(130); + buffer.putUint8(131); writeValue(buffer, value.encode()); } else if (value is ImageSelectionOptions) { - buffer.putUint8(131); + buffer.putUint8(132); writeValue(buffer, value.encode()); } else if (value is MediaSelectionOptions) { - buffer.putUint8(132); + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is ImagePickerSuccessResult) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is ImagePickerErrorResult) { + buffer.putUint8(135); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -41,13 +50,20 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - return GeneralOptions.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : ImagePickerError.values[value]; case 130: - return MaxSize.decode(readValue(buffer)!); + return GeneralOptions.decode(readValue(buffer)!); case 131: - return ImageSelectionOptions.decode(readValue(buffer)!); + return MaxSize.decode(readValue(buffer)!); case 132: + return ImageSelectionOptions.decode(readValue(buffer)!); + case 133: return MediaSelectionOptions.decode(readValue(buffer)!); + case 134: + return ImagePickerSuccessResult.decode(readValue(buffer)!); + case 135: + return ImagePickerErrorResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -59,15 +75,17 @@ abstract class TestHostImagePickerApi { TestDefaultBinaryMessengerBinding.instance; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + /// Returns whether [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller) + /// is supported on the current macOS version. bool supportsPHPicker(); - Future> pickImages( + Future pickImages( ImageSelectionOptions options, GeneralOptions generalOptions); /// Currently, multi-video selection is unimplemented. - Future> pickVideos(GeneralOptions generalOptions); + Future pickVideos(GeneralOptions generalOptions); - Future> pickMedia( + Future pickMedia( MediaSelectionOptions options, GeneralOptions generalOptions); static void setUp( @@ -129,7 +147,7 @@ abstract class TestHostImagePickerApi { assert(arg_generalOptions != null, 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.'); try { - final List output = + final ImagePickerResult output = await api.pickImages(arg_options!, arg_generalOptions!); return [output]; } on PlatformException catch (e) { @@ -163,7 +181,7 @@ abstract class TestHostImagePickerApi { assert(arg_generalOptions != null, 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.'); try { - final List output = + final ImagePickerResult output = await api.pickVideos(arg_generalOptions!); return [output]; } on PlatformException catch (e) { @@ -201,7 +219,7 @@ abstract class TestHostImagePickerApi { assert(arg_generalOptions != null, 'Argument for dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.'); try { - final List output = + final ImagePickerResult output = await api.pickMedia(arg_options!, arg_generalOptions!); return [output]; } on PlatformException catch (e) {