├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── flutter_bug_report.yml │ ├── flutter_discussions.yml │ ├── flutter_how_to_use.yml │ └── flutter_new_feature.yml └── workflows │ ├── build-example-apk.yml │ ├── check-compatibility.yml │ ├── issue-triage.yml │ ├── issues.yml │ ├── publish.yml │ ├── remove-issue-label.yml │ └── runnable.yml ├── .gitignore ├── .markdownlint.jsonc ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── MIGRATION_GUIDE.md ├── README-ZH.md ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── build.gradle ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── fluttercandies │ └── photo_manager │ ├── PhotoManagerPlugin.kt │ ├── constant │ ├── AssetType.kt │ └── Methods.kt │ ├── core │ ├── PhotoManager.kt │ ├── PhotoManagerDeleteManager.kt │ ├── PhotoManagerNotifyChannel.kt │ ├── PhotoManagerPlugin.kt │ ├── cache │ │ └── ScopedCache.kt │ ├── entity │ │ ├── AssetEntity.kt │ │ ├── AssetPathEntity.kt │ │ ├── PermissionResult.kt │ │ ├── ThumbLoadOption.kt │ │ └── filter │ │ │ ├── CommonFilterOption.kt │ │ │ ├── CustomOption.kt │ │ │ └── FilterOption.kt │ └── utils │ │ ├── AndroidQDBUtils.kt │ │ ├── CommonExt.kt │ │ ├── ConvertUtils.kt │ │ ├── DBUtils.kt │ │ ├── IDBUtils.kt │ │ ├── MediaStoreUtils.kt │ │ ├── RequestTypeUtils.kt │ │ └── VideoUtils.kt │ ├── permission │ ├── PermissionDelegate.kt │ ├── PermissionsListener.kt │ ├── PermissionsUtils.kt │ └── impl │ │ ├── PermissionDelegate19.kt │ │ ├── PermissionDelegate23.kt │ │ ├── PermissionDelegate29.kt │ │ ├── PermissionDelegate33.kt │ │ └── PermissionDelegate34.kt │ ├── thumb │ └── ThumbnailUtil.kt │ └── util │ ├── LogUtils.kt │ └── ResultHandler.kt ├── copy_on_publish.sh ├── example ├── .gitignore ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── fluttercandies │ │ │ │ └── photo_manager_example │ │ │ │ ├── ExampleAppGlideModule.kt │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── photo-manager-keystore │ └── settings.gradle ├── custom.dart ├── devtools_options.yaml ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── main.dart │ ├── model │ │ └── photo_provider.dart │ ├── page │ │ ├── change_notify_page.dart │ │ ├── copy_to_another_gallery_example.dart │ │ ├── custom_filter │ │ │ ├── advance_filter_page.dart │ │ │ ├── custom_filter_sql_gif_image.dart │ │ │ ├── custom_filter_sql_page.dart │ │ │ ├── filter_assets_page.dart │ │ │ ├── image_list.dart │ │ │ ├── include_hidden_test_page.dart │ │ │ ├── order_by_action.dart │ │ │ ├── path_list.dart │ │ │ ├── path_page.dart │ │ │ └── where_action.dart │ │ ├── custom_filter_example_page.dart │ │ ├── detail_page.dart │ │ ├── developer │ │ │ ├── android │ │ │ │ └── column_names_page.dart │ │ │ ├── create_entity_by_id.dart │ │ │ ├── custom_filter_page.dart │ │ │ ├── dev_title_page.dart │ │ │ ├── develop_index_page.dart │ │ │ ├── ios │ │ │ │ ├── create_folder_example.dart │ │ │ │ └── edit_asset.dart │ │ │ ├── issues_page │ │ │ │ ├── issue_1025.dart │ │ │ │ ├── issue_1031.dart │ │ │ │ ├── issue_1051.dart │ │ │ │ ├── issue_1053.dart │ │ │ │ ├── issue_1152.dart │ │ │ │ ├── issue_1271_demo.dart │ │ │ │ ├── issue_734.dart │ │ │ │ ├── issue_918.dart │ │ │ │ ├── issue_962.dart │ │ │ │ ├── issue_979.dart │ │ │ │ ├── issue_988.dart │ │ │ │ └── issue_index_page.dart │ │ │ ├── permission_state_page.dart │ │ │ ├── remove_all_android_not_exists_example.dart │ │ │ └── verbose_log_page.dart │ │ ├── filter_option_page.dart │ │ ├── gallery_list_page.dart │ │ ├── home_page.dart │ │ ├── image_list_page.dart │ │ ├── index_page.dart │ │ ├── move_to_another_gallery_example.dart │ │ ├── save_image_example.dart │ │ └── sub_gallery_page.dart │ ├── util │ │ ├── asset_utils.dart │ │ ├── common_util.dart │ │ ├── log.dart │ │ └── log_export.dart │ └── widget │ │ ├── change_notifier_builder.dart │ │ ├── dialog │ │ ├── duration_picker.dart │ │ └── list_dialog.dart │ │ ├── gallery_item_widget.dart │ │ ├── image_item_widget.dart │ │ ├── live_photos_widget.dart │ │ ├── loading_widget.dart │ │ ├── nav_button.dart │ │ ├── nav_column.dart │ │ ├── theme_button.dart │ │ └── video_widget.dart ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ └── Flutter-Release.xcconfig │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ └── MainMenu.xib │ │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements └── pubspec.yaml ├── example_ohos ├── .gitignore ├── README.md ├── analysis_options.yaml ├── custom.dart ├── lib │ └── main.dart ├── ohos │ ├── .gitignore │ ├── AppScope │ │ ├── app.json5 │ │ └── resources │ │ │ └── base │ │ │ ├── element │ │ │ └── string.json │ │ │ └── media │ │ │ └── app_icon.png │ ├── build-profile.json5 │ ├── entry │ │ ├── .gitignore │ │ ├── build-profile.json5 │ │ ├── hvigorfile.ts │ │ ├── oh-package.json5 │ │ └── src │ │ │ └── main │ │ │ ├── ets │ │ │ ├── entryability │ │ │ │ └── EntryAbility.ets │ │ │ ├── pages │ │ │ │ └── Index.ets │ │ │ └── plugins │ │ │ │ └── GeneratedPluginRegistrant.ets │ │ │ ├── module.json5 │ │ │ └── resources │ │ │ ├── base │ │ │ ├── element │ │ │ │ ├── color.json │ │ │ │ └── string.json │ │ │ ├── media │ │ │ │ └── icon.png │ │ │ └── profile │ │ │ │ └── main_pages.json │ │ │ ├── en_US │ │ │ └── element │ │ │ │ └── string.json │ │ │ └── zh_CN │ │ │ └── element │ │ │ └── string.json │ ├── hvigor │ │ ├── hvigor-config.json5 │ │ └── hvigor-wrapper.js │ ├── hvigorfile.ts │ ├── hvigorw │ ├── hvigorw.bat │ └── oh-package.json5 └── pubspec.yaml ├── flow_chart ├── advance_custom_filter.dio └── advance_custom_filter.png ├── ios ├── .gitignore ├── Classes │ ├── PMConverter.h │ ├── PMConverter.m │ ├── PMImport.h │ ├── PMNotificationManager.h │ ├── PMNotificationManager.m │ ├── PMPlugin.h │ ├── PMPlugin.m │ ├── PMProgressHandler.h │ ├── PMProgressHandler.m │ ├── PhotoManagerPlugin.h │ ├── PhotoManagerPlugin.m │ ├── ResultHandler.h │ ├── ResultHandler.m │ └── core │ │ ├── AssetEntity.h │ │ ├── AssetEntity.m │ │ ├── NSString+PM_COMMON.h │ │ ├── NSString+PM_COMMON.m │ │ ├── PHAsset+PM_COMMON.h │ │ ├── PHAsset+PM_COMMON.m │ │ ├── PHAssetCollection+PM_COMMON.h │ │ ├── PHAssetCollection+PM_COMMON.m │ │ ├── PHAssetResource+PM_COMMON.h │ │ ├── PHAssetResource+PM_COMMON.m │ │ ├── PMAssetPathEntity.h │ │ ├── PMAssetPathEntity.m │ │ ├── PMBaseFilter.h │ │ ├── PMCacheContainer.h │ │ ├── PMCacheContainer.m │ │ ├── PMConvertProtocol.h │ │ ├── PMConvertUtils.h │ │ ├── PMConvertUtils.m │ │ ├── PMFileHelper.h │ │ ├── PMFileHelper.m │ │ ├── PMFilterOption.h │ │ ├── PMFilterOption.m │ │ ├── PMFolderUtils.h │ │ ├── PMFolderUtils.m │ │ ├── PMImageUtil.h │ │ ├── PMImageUtil.m │ │ ├── PMLogUtils.h │ │ ├── PMLogUtils.m │ │ ├── PMMD5Utils.h │ │ ├── PMMD5Utils.m │ │ ├── PMManager.h │ │ ├── PMManager.m │ │ ├── PMPathFilterOption.h │ │ ├── PMPathFilterOption.m │ │ ├── PMProgressHandlerProtocol.h │ │ ├── PMRequestTypeUtils.h │ │ ├── PMRequestTypeUtils.m │ │ ├── PMResultHandler.h │ │ ├── PMThumbLoadOption.h │ │ ├── PMThumbLoadOption.m │ │ ├── Reply.h │ │ └── Reply.m ├── Resources │ └── PrivacyInfo.xcprivacy └── photo_manager.podspec ├── lib ├── photo_manager.dart ├── platform_utils.dart └── src │ ├── filter │ ├── base_filter.dart │ ├── classical │ │ ├── filter_option_group.dart │ │ └── filter_options.dart │ ├── custom │ │ ├── advance.dart │ │ ├── custom_columns.dart │ │ ├── custom_filter.dart │ │ └── order_by_item.dart │ └── path_filter.dart │ ├── internal │ ├── constants.dart │ ├── editor.dart │ ├── enums.dart │ ├── extensions.dart │ ├── map_interface.dart │ ├── plugin.dart │ └── progress_handler.dart │ ├── managers │ ├── caching_manager.dart │ ├── notify_manager.dart │ └── photo_manager.dart │ ├── types │ ├── entity.dart │ ├── thumbnail.dart │ └── types.dart │ └── utils │ ├── column_utils.dart │ └── convert_utils.dart ├── log ├── README.md ├── android │ ├── android-28-delete.log │ ├── android-28-info.log │ ├── android-28-insert.log │ ├── android-30.info.log │ ├── android-q-delete.log │ ├── android-q-insert.log │ ├── androidq-info.log │ └── sql-28.log └── iOS │ ├── ios-create.log │ └── ios-delete.log ├── macos ├── ohos ├── .gitignore ├── build-profile.json5 ├── hvigorfile.ts ├── index.ets ├── oh-package.json5 └── src │ └── main │ ├── ets │ └── components │ │ └── plugin │ │ ├── PhotoManagerNotifyPlugin.ets │ │ ├── PhotoManagerPlugin.ets │ │ ├── filter │ │ └── FilterOption.ets │ │ ├── handlers │ │ ├── AlbumHandler.ets │ │ ├── HandlerBase.ets │ │ ├── PermissionHandler.ets │ │ ├── PhotoAssetHandler.ets │ │ └── SaveHandler.ets │ │ ├── types │ │ └── RequestType.ets │ │ └── utils │ │ ├── EnumUtils.ets │ │ └── MapUtils.ets │ └── module.json5 ├── pubspec.yaml ├── scripts └── check_license.dart └── test ├── internal └── extensions_test.dart └── photo_manager_test.dart /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force batch scripts to always use CRLF line endings so that if a repo is accessed 2 | # in Windows via a file share from Linux, the scripts will work. 3 | *.{cmd,[cC][mM][dD]} text eol=crlf 4 | *.{bat,[bB][aA][tT]} text eol=crlf 5 | 6 | # Force bash scripts to always use LF line endings so that if a repo is accessed 7 | # in Unix via a file share from Windows, the scripts will work. 8 | *.sh text eol=lf 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/flutter_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Flutter plugin bug report 2 | description: bug report 3 | title: "[Bug report] " 4 | body: 5 | - type: input 6 | id: version 7 | attributes: 8 | label: Version 9 | description: Version of the plugin 10 | placeholder: ex. 2.5.2 11 | validations: 12 | required: true 13 | - type: dropdown 14 | id: platforms 15 | attributes: 16 | label: Platforms 17 | description: Check all that apply 18 | multiple: true 19 | options: 20 | - dart 21 | - Android 22 | - iOS 23 | - macOS 24 | validations: 25 | required: true 26 | - type: input 27 | id: device-model 28 | attributes: 29 | label: Device Model 30 | description: "Device Model (ex. iPhone 12(iOS 14), Galaxy S21(Android 11))" 31 | placeholder: ex. iPhone 12 (iOS 14) 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: flutter-doctor 36 | attributes: 37 | label: flutter info 38 | description: "flutter doctor -v in your terminal" 39 | placeholder: ex. flutter doctor -v 40 | render: sh 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: what-happened 45 | attributes: 46 | label: "How to reproduce?" 47 | description: "How to reproduce the problem?" 48 | placeholder: "ex. I found a bug when I call the PhotoManager.getAssetPathList method." 49 | validations: 50 | required: true 51 | - type: textarea 52 | id: log 53 | attributes: 54 | label: Logs 55 | description: "Error log" 56 | placeholder: ex. flutter run --verbose 57 | render: sh 58 | validations: 59 | required: false 60 | - type: textarea 61 | id: example-code 62 | attributes: 63 | label: "Example code (optional)" 64 | description: "If you can, please provide a minimal example to reproduce the problem." 65 | placeholder: "ex. PhotoManager.editor.saveImageWithId(id, quality: 100)" 66 | render: Dart 67 | validations: 68 | required: false 69 | - type: input 70 | id: contact 71 | attributes: 72 | label: Contact 73 | description: Your contace (no requird) 74 | placeholder: ex. email@example.com / twitter / github 75 | validations: 76 | required: false 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/flutter_discussions.yml: -------------------------------------------------------------------------------- 1 | name: Discussions 2 | description: Discussions 3 | title: "[Discussions] " 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | First check whether the project has Discussions enabled. 9 | If so, it is recommended to use Discussions to create discussions. 10 | The discussions is here: 11 | [Discussions](https://github.com/FlutterCandies/flutter_photo_manager/discussions) 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/flutter_how_to_use.yml: -------------------------------------------------------------------------------- 1 | name: How to use 2 | description: How to use the Flutter plugin 3 | title: "[How to use] " 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this issue! 9 | You need read document before you ask question. 10 | If you can, search the issue list 11 | or [StackOverflow](https://stackoverflow.com/questions/tagged/flutter) 12 | to see if someone has asked the same question. 13 | - type: dropdown 14 | id: platforms 15 | attributes: 16 | label: Platforms 17 | description: Check all that apply 18 | multiple: true 19 | options: 20 | - dart 21 | - Android 22 | - iOS 23 | - macOS 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: description 28 | attributes: 29 | label: Description 30 | description: "Describe the feature you want to be added" 31 | placeholder: "ex. I want to add a new method to get the thumbnail of the asset." 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: my-code 36 | attributes: 37 | label: My code 38 | description: "I want to know how to use the method." 39 | placeholder: | 40 | 'ex. I want to get the thumbnail of the asset to display it in the list.' 41 | validations: 42 | required: false 43 | - type: textarea 44 | id: try-do-it 45 | attributes: 46 | label: Try do it 47 | description: "I have tried to do it, but I don't know how to do it." 48 | placeholder: | 49 | 'ex. I want to get the thumbnail of the asset to display it in the list.' 50 | validations: 51 | required: false 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/flutter_new_feature.yml: -------------------------------------------------------------------------------- 1 | name: "Flutter new feature request" 2 | description: "Request a new feature for the Flutter plugin" 3 | title: "[Feature request] " 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this feature request! 9 | Before submitting, make sure you read the document and search the issue list. 10 | If you are not sure, you can run the example code 11 | first to understand the example that may be your requirement. 12 | - type: dropdown 13 | id: platforms 14 | attributes: 15 | label: Platforms 16 | description: Check all that apply 17 | multiple: true 18 | options: 19 | - dart 20 | - Android 21 | - iOS 22 | - macOS 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: description 27 | attributes: 28 | label: Description 29 | description: "Describe the feature you want to be added" 30 | placeholder: "ex. I want to add a new method to get the thumbnail of the asset." 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: why 35 | attributes: 36 | label: Why 37 | description: "Why do you want this feature to be added? What's the use case?" 38 | placeholder: | 39 | ex. I want to get the thumbnail of the asset to display it in the list. 40 | validations: 41 | required: false 42 | - type: markdown 43 | attributes: 44 | value: | 45 | Add as many or as many reasons as possible, 46 | which will give priority to feature request 47 | -------------------------------------------------------------------------------- /.github/workflows/build-example-apk.yml: -------------------------------------------------------------------------------- 1 | name: Build example apk 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | release: 9 | types: 10 | - created 11 | 12 | permissions: 13 | contents: write 14 | 15 | env: 16 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | jobs: 19 | build-apk: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-java@v4 24 | with: 25 | distribution: 'zulu' 26 | java-version: '17' 27 | - uses: flutter-actions/setup-flutter@v4 28 | with: 29 | channel: 'stable' 30 | cache: true 31 | - run: flutter doctor -v 32 | name: Flutter info 33 | - run: flutter build apk --release 34 | name: Build apk 35 | working-directory: ${{ github.workspace }}/example 36 | - name: Upload apk to release ${{ github.event.release.tag_name }} 37 | run: | 38 | gh release upload ${{ github.event.release.tag_name }} build/app/outputs/flutter-apk/*.apk 39 | echo "Show apk download url: " 40 | gh release view ${{ github.event.release.tag_name }} --json assets --jq '.assets.[].url' 41 | working-directory: ${{ github.workspace }}/example 42 | -------------------------------------------------------------------------------- /.github/workflows/issue-triage.yml: -------------------------------------------------------------------------------- 1 | # For more information, see 2 | # https://github.com/fluttercandies/triage_bot_for_flutter_photo_manager 3 | name: Issue Triage Bot 4 | 5 | # Run when an issue is created. 6 | on: 7 | issues: 8 | types: 9 | - opened 10 | 11 | # All permissions not specified are set to 'none'. 12 | permissions: 13 | issues: write 14 | 15 | jobs: 16 | triage-issues: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | repository: fluttercandies/triage_bot_for_flutter_photo_manager 22 | - uses: dart-lang/setup-dart@v1 23 | - run: dart pub get 24 | working-directory: pkgs/issue_triage_bot 25 | 26 | # Delay 2 minutes to allow a grace period between opening or transferring 27 | # an issue and assigning a label. 28 | - name: sleep 2m 29 | run: sleep 120 30 | 31 | - name: triage issue 32 | working-directory: pkgs/issue_triage_bot 33 | env: 34 | ISSUE_URL: ${{ github.event.issue.html_url }} 35 | GITHUB_TOKEN: ${{ secrets.TRIAGE_BOT_TOKEN }} 36 | GEMINI_API_KEY: ${{ secrets.TRIAGE_BOT_API_KEY }} 37 | run: dart bin/triage.dart $ISSUE_URL --release 38 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | name: Auto add label to new issue 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | 7 | jobs: 8 | add-label: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - uses: actions/github-script@v3 14 | name: Add label to new issue 15 | with: 16 | github-token: ${{ secrets.GITHUB_TOKEN }} 17 | script: | 18 | github.issues.addLabels({ 19 | issue_number: context.issue.number, 20 | owner: context.repo.owner, 21 | repo: context.repo.repo, 22 | labels: ['await triage'] 23 | }) 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Copy on publish 15 | run: ./copy_on_publish.sh 16 | - name: Publish 17 | uses: sakebook/actions-flutter-pub-publisher@v1.3.1 18 | with: 19 | credential: ${{ secrets.CREDENTIAL_JSON }} 20 | flutter_package: true 21 | skip_test: true 22 | dry_run: false 23 | -------------------------------------------------------------------------------- /.github/workflows/remove-issue-label.yml: -------------------------------------------------------------------------------- 1 | name: Remove label when issue is closed 2 | on: 3 | issues: 4 | types: 5 | - closed 6 | 7 | jobs: 8 | add-label: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | env: 13 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | steps: 15 | - name: remove label 16 | run: 17 | gh issue edit ${{ github.event.issue.number }} --remove-label 'await triage' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | pubspec.lock 7 | 8 | build/ 9 | .idea/ 10 | .vscode/ 11 | android/.classpath 12 | android/.project 13 | 14 | fvm 15 | doc/ 16 | .env 17 | *.iml 18 | .metadata 19 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "MD033": false, 3 | "MD013": false, 4 | "MD004": false, 5 | "MD024": false 6 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @CaiJingLong @AlexV525 2 | ohos/ @zmtzawqlp 3 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | errors: 5 | always_declare_return_types: error 6 | always_put_control_body_on_new_line: warning 7 | avoid_renaming_method_parameters: error 8 | avoid_void_async: error 9 | camel_case_types: error 10 | constant_identifier_names: error 11 | deprecated_member_use_from_same_package: ignore 12 | non_constant_identifier_names: error 13 | prefer_single_quotes: warning 14 | require_trailing_commas: warning 15 | invalid_annotation_target: false 16 | 17 | linter: 18 | rules: 19 | always_declare_return_types: true 20 | always_put_control_body_on_new_line: true 21 | avoid_print: true 22 | avoid_unnecessary_containers: true 23 | avoid_void_async: true 24 | curly_braces_in_flow_control_structures: true 25 | directives_ordering: true 26 | prefer_const_constructors: true 27 | prefer_const_constructors_in_immutables: false 28 | prefer_final_fields: true 29 | prefer_final_in_for_each: true 30 | prefer_final_locals: true 31 | prefer_single_quotes: true 32 | require_trailing_commas: true 33 | sort_child_properties_last: true 34 | sort_constructors_first: true 35 | sort_unnamed_constructors_first: true 36 | unnecessary_await_in_return: true 37 | unnecessary_late: true 38 | unnecessary_parenthesis: true 39 | use_build_context_synchronously: false 40 | void_checks: true 41 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | /gradlew 10 | /gradlew.bat 11 | .classpath 12 | .project 13 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | File pubspec = new File(project.projectDir.parentFile, 'pubspec.yaml') 2 | String yaml = pubspec.text 3 | java.util.regex.Matcher versionMatcher = java.util.regex.Pattern.compile("^version:\\s*['|\"]?([^\\n|'|\"]*)['|\"]?\$", java.util.regex.Pattern.MULTILINE).matcher(yaml) 4 | versionMatcher.find() 5 | String library_version = versionMatcher.group(1).replaceAll("\\+", "-") 6 | 7 | group 'com.fluttercandies.photo_manager' 8 | version library_version 9 | 10 | buildscript { 11 | ext.kotlin_version = '1.9.23' 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 19 | } 20 | } 21 | 22 | rootProject.allprojects { 23 | repositories { 24 | google() 25 | mavenCentral() 26 | } 27 | } 28 | 29 | apply plugin: 'com.android.library' 30 | apply plugin: 'kotlin-android' 31 | 32 | android { 33 | if (project.android.hasProperty('namespace')) { 34 | namespace 'com.flutterandies.photo_manager' 35 | } 36 | 37 | compileSdkVersion 34 38 | 39 | defaultConfig { 40 | minSdkVersion 16 41 | } 42 | 43 | compileOptions { 44 | sourceCompatibility JavaVersion.VERSION_17 45 | targetCompatibility JavaVersion.VERSION_17 46 | } 47 | 48 | kotlinOptions { 49 | jvmTarget = '17' 50 | } 51 | 52 | sourceSets { 53 | main.java.srcDirs += 'src/main/kotlin' 54 | } 55 | } 56 | 57 | dependencies { 58 | implementation 'com.github.bumptech.glide:glide:4.15.1' 59 | implementation 'androidx.exifinterface:exifinterface:1.3.7' 60 | } 61 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'photo_manager' 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/constant/AssetType.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.constant 2 | 3 | enum class AssetType { Image, Video, Audio } 4 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/AssetEntity.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.entity 2 | 3 | import android.net.Uri 4 | import com.fluttercandies.photo_manager.core.utils.IDBUtils.Companion.isAboveAndroidQ 5 | import com.fluttercandies.photo_manager.core.utils.MediaStoreUtils 6 | import java.io.File 7 | 8 | data class AssetEntity( 9 | val id: Long, 10 | val path: String, 11 | val duration: Long, 12 | val createDt: Long, 13 | val width: Int, 14 | val height: Int, 15 | val type: Int, 16 | val displayName: String, 17 | val modifiedDate: Long, 18 | val orientation: Int, 19 | val lat: Double? = null, 20 | val lng: Double? = null, 21 | val androidQRelativePath: String? = null, 22 | val mimeType: String? = null 23 | ) { 24 | fun getUri(): Uri = MediaStoreUtils.getUri( 25 | id, 26 | MediaStoreUtils.convertTypeToMediaType(type) 27 | ) 28 | 29 | val relativePath: String? = if (isAboveAndroidQ) { 30 | androidQRelativePath 31 | } else { 32 | File(path).parent 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/AssetPathEntity.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.entity 2 | 3 | data class AssetPathEntity( 4 | val id: String, 5 | val name: String, 6 | var assetCount: Int, 7 | val typeInt: Int, 8 | var isAll: Boolean = false, 9 | var modifiedDate: Long? = null 10 | ) 11 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/PermissionResult.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.entity 2 | 3 | enum class PermissionResult(val value: Int) { 4 | NotDetermined(0), 5 | Denied(2), 6 | Authorized(3), 7 | Limited(4), 8 | } 9 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/ThumbLoadOption.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.entity 2 | 3 | import android.graphics.Bitmap 4 | 5 | data class ThumbLoadOption( 6 | val width: Int, 7 | val height: Int, 8 | val format: Bitmap.CompressFormat, 9 | val quality: Int, 10 | val frame: Long, 11 | ) { 12 | companion object Factory { 13 | fun fromMap(map: Map<*, *>): ThumbLoadOption { 14 | val width = map["width"] as Int 15 | val height = map["height"] as Int 16 | val format = map["format"] as Int 17 | val quality = map["quality"] as Int 18 | val frame = (map["frame"] as Int).toLong() 19 | 20 | val compressFormat = if (format == 0) { 21 | Bitmap.CompressFormat.JPEG 22 | } else { 23 | Bitmap.CompressFormat.PNG 24 | } 25 | 26 | return ThumbLoadOption(width, height, compressFormat, quality, frame) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/CustomOption.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.entity.filter 2 | 3 | import com.fluttercandies.photo_manager.core.utils.RequestTypeUtils 4 | 5 | class CustomOption(private val map: Map<*, *>) : FilterOption() { 6 | 7 | override val containsPathModified: Boolean = map["containsPathModified"] as Boolean 8 | 9 | override fun orderByCondString(): String? { 10 | val list = map["orderBy"] as? List<*> 11 | if (list.isNullOrEmpty()) { 12 | return null 13 | } 14 | return list.joinToString(",") { 15 | val map = it as Map<*, *> 16 | val column = map["column"] as String 17 | val isAsc = map["isAsc"] as Boolean 18 | "$column ${if (isAsc) "ASC" else "DESC"}" 19 | } 20 | } 21 | 22 | override fun makeWhere(requestType: Int, args: ArrayList, needAnd: Boolean): String { 23 | val where = map["where"] as String 24 | 25 | val typeWhere = RequestTypeUtils.toWhere(requestType) 26 | 27 | if (where.trim().isEmpty()) { 28 | if (needAnd) { 29 | return "AND $typeWhere" 30 | } 31 | 32 | return typeWhere 33 | } 34 | 35 | if (needAnd && where.trim().isNotEmpty()) { 36 | return "AND ( $where )" 37 | } 38 | return "( $where )" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/FilterOption.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.entity.filter 2 | 3 | abstract class FilterOption { 4 | abstract val containsPathModified: Boolean 5 | 6 | abstract fun orderByCondString(): String? 7 | 8 | abstract fun makeWhere( 9 | requestType: Int, 10 | args: ArrayList, 11 | needAnd: Boolean = true 12 | ): String 13 | } 14 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/CommonExt.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.utils 2 | 3 | import androidx.exifinterface.media.ExifInterface 4 | import java.io.File 5 | import java.io.InputStream 6 | 7 | /** 8 | * Create the directory if it's not exist. 9 | */ 10 | fun String.checkDirs() { 11 | val targetFile = File(this) 12 | if (!targetFile.parentFile!!.exists()) { 13 | targetFile.parentFile!!.mkdirs() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/MediaStoreUtils.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.utils 2 | 3 | import android.content.ContentUris 4 | import android.net.Uri 5 | import android.provider.MediaStore 6 | 7 | object MediaStoreUtils { 8 | fun getInsertUri(mediaType: Int): Uri { 9 | return when (mediaType) { 10 | MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 11 | MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI 12 | MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI 13 | else -> IDBUtils.allUri 14 | } 15 | } 16 | 17 | fun getUri(id: Long, mediaType: Int): Uri { 18 | return ContentUris.withAppendedId(getInsertUri(mediaType), id) 19 | } 20 | 21 | fun convertTypeToMediaType(type: Int): Int { 22 | return when (type) { 23 | 1 -> MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE 24 | 2 -> MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO 25 | 3 -> MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO 26 | else -> 0 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/RequestTypeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.utils 2 | 3 | import android.provider.MediaStore 4 | 5 | object RequestTypeUtils { 6 | private const val typeImage = 1 7 | private const val typeVideo = 1.shl(1) 8 | private const val typeAudio = 1.shl(2) 9 | 10 | fun containsImage(type: Int): Boolean = checkType(type, typeImage) 11 | 12 | fun containsVideo(type: Int): Boolean = checkType(type, typeVideo) 13 | 14 | fun containsAudio(type: Int): Boolean = checkType(type, typeAudio) 15 | 16 | private fun checkType(type: Int, targetType: Int): Boolean { 17 | return type and targetType == targetType 18 | } 19 | 20 | fun toWhere(requestType: Int): String { 21 | val typeInt = arrayListOf() 22 | if (containsImage(requestType)) { 23 | typeInt.add(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 24 | } 25 | if (containsAudio(requestType)) { 26 | typeInt.add(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO) 27 | } 28 | if (containsVideo(requestType)) { 29 | typeInt.add(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) 30 | } 31 | 32 | val where = typeInt.joinToString(" OR ") { 33 | "${MediaStore.Files.FileColumns.MEDIA_TYPE} = $it" 34 | } 35 | 36 | return "( $where )" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/VideoUtils.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.core.utils 2 | 3 | import android.media.MediaPlayer 4 | 5 | object VideoUtils { 6 | data class VideoInfo(var width: Int?, var height: Int?, var duration: Int?) 7 | 8 | fun getPropertiesUseMediaPlayer(path: String): VideoInfo { 9 | val mediaPlayer = MediaPlayer() 10 | mediaPlayer.setDataSource(path) 11 | mediaPlayer.setOnErrorListener { _, _, _ -> true } 12 | try { 13 | mediaPlayer.prepare() 14 | } catch (e: Throwable) { 15 | mediaPlayer.release() 16 | return VideoInfo(null, null, null) 17 | } 18 | mediaPlayer.videoHeight 19 | val info = VideoInfo(mediaPlayer.videoWidth, mediaPlayer.videoHeight, mediaPlayer.duration) 20 | 21 | mediaPlayer.stop() 22 | mediaPlayer.release() 23 | 24 | return info 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/permission/PermissionsListener.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.permission 2 | 3 | interface PermissionsListener { 4 | fun onGranted(needPermissions: MutableList) 5 | 6 | fun onDenied( 7 | deniedPermissions: MutableList, 8 | grantedPermissions: MutableList, 9 | needPermissions: MutableList 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/permission/impl/PermissionDelegate19.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.permission.impl 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.fluttercandies.photo_manager.core.entity.PermissionResult 6 | import com.fluttercandies.photo_manager.permission.PermissionDelegate 7 | import com.fluttercandies.photo_manager.permission.PermissionsUtils 8 | 9 | class PermissionDelegate19 : PermissionDelegate() { 10 | override fun requestPermission( 11 | permissionsUtils: PermissionsUtils, 12 | context: Context, 13 | requestType: Int, 14 | mediaLocation: Boolean 15 | ) { 16 | permissionsUtils.permissionsListener?.onGranted(mutableListOf()) 17 | } 18 | 19 | override fun havePermissions(context: Context, requestType: Int): Boolean { 20 | return true 21 | } 22 | 23 | override fun haveMediaLocation(context: Context): Boolean { 24 | return true 25 | } 26 | 27 | override fun getAuthValue( 28 | context: Application, requestType: Int, mediaLocation: Boolean 29 | ): PermissionResult { 30 | return PermissionResult.Authorized 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/permission/impl/PermissionDelegate23.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.permission.impl 2 | 3 | import android.Manifest 4 | import android.app.Application 5 | import android.content.Context 6 | import androidx.annotation.RequiresApi 7 | import com.fluttercandies.photo_manager.core.entity.PermissionResult 8 | import com.fluttercandies.photo_manager.permission.PermissionDelegate 9 | import com.fluttercandies.photo_manager.permission.PermissionsUtils 10 | 11 | @RequiresApi(23) 12 | class PermissionDelegate23 : PermissionDelegate() { 13 | companion object { 14 | private const val readPermission = Manifest.permission.READ_EXTERNAL_STORAGE 15 | private const val writePermission = Manifest.permission.WRITE_EXTERNAL_STORAGE 16 | } 17 | 18 | override fun requestPermission( 19 | permissionsUtils: PermissionsUtils, 20 | context: Context, 21 | requestType: Int, 22 | mediaLocation: Boolean 23 | ) { 24 | val permissions = mutableListOf(readPermission, writePermission) 25 | 26 | if (havePermissions(context, requestType)) { 27 | permissionsUtils.permissionsListener?.onGranted(permissions) 28 | } else { 29 | requestPermission(permissionsUtils, permissions) 30 | } 31 | } 32 | 33 | override fun havePermissions(context: Context, requestType: Int): Boolean { 34 | val requireWritePermission = havePermissionInManifest(context, writePermission) 35 | val validWritePermission = 36 | !requireWritePermission || havePermission(context, writePermission) 37 | return havePermission(context, readPermission) && validWritePermission 38 | } 39 | 40 | override fun haveMediaLocation(context: Context): Boolean { 41 | return true 42 | } 43 | 44 | override fun getAuthValue( 45 | context: Application, 46 | requestType: Int, 47 | mediaLocation: Boolean 48 | ): PermissionResult { 49 | return if (havePermissions(context, requestType)) { 50 | PermissionResult.Authorized 51 | } else { 52 | PermissionResult.Denied 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/permission/impl/PermissionDelegate29.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.permission.impl 2 | 3 | import android.Manifest 4 | import android.app.Application 5 | import android.content.Context 6 | import android.os.Build 7 | import androidx.annotation.RequiresApi 8 | import com.fluttercandies.photo_manager.core.entity.PermissionResult 9 | import com.fluttercandies.photo_manager.permission.PermissionDelegate 10 | import com.fluttercandies.photo_manager.permission.PermissionsUtils 11 | 12 | @RequiresApi(Build.VERSION_CODES.Q) 13 | class PermissionDelegate29 : PermissionDelegate() { 14 | 15 | companion object { 16 | private const val readPermission = Manifest.permission.READ_EXTERNAL_STORAGE 17 | private const val mediaLocationPermission = Manifest.permission.ACCESS_MEDIA_LOCATION 18 | } 19 | 20 | override fun requestPermission( 21 | permissionsUtils: PermissionsUtils, 22 | context: Context, 23 | requestType: Int, 24 | mediaLocation: Boolean 25 | ) { 26 | val permissions = mutableListOf(readPermission) 27 | 28 | if (mediaLocation) { 29 | permissions.add(mediaLocationPermission) 30 | } 31 | 32 | if (havePermissions(context, *permissions.toTypedArray())) { 33 | permissionsUtils.permissionsListener?.onGranted(permissions) 34 | } else { 35 | requestPermission(permissionsUtils, permissions) 36 | } 37 | } 38 | 39 | override fun havePermissions(context: Context, requestType: Int): Boolean { 40 | return havePermission(context, readPermission) 41 | } 42 | 43 | override fun haveMediaLocation(context: Context): Boolean { 44 | return havePermission(context, mediaLocationPermission) 45 | } 46 | 47 | override fun getAuthValue( 48 | context: Application, 49 | requestType: Int, 50 | mediaLocation: Boolean 51 | ): PermissionResult { 52 | return if (havePermissions(context, readPermission)) { 53 | PermissionResult.Authorized 54 | } else { 55 | PermissionResult.Denied 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/fluttercandies/photo_manager/util/ResultHandler.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager.util 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import io.flutter.plugin.common.MethodCall 6 | import io.flutter.plugin.common.MethodChannel 7 | 8 | class ResultHandler(var result: MethodChannel.Result, val call: MethodCall) { 9 | init { 10 | handler.hasMessages(0) // just do it to init handler 11 | } 12 | 13 | companion object { 14 | @JvmField 15 | val handler = Handler(Looper.getMainLooper()) 16 | } 17 | 18 | private var isReplied = false 19 | 20 | fun reply(any: Any?) { 21 | if (isReplied) { 22 | return 23 | } 24 | isReplied = true 25 | val result = this.result 26 | handler.post { 27 | try { 28 | result.success(any) 29 | } catch (e: IllegalStateException) { 30 | // Do nothing 31 | } 32 | } 33 | } 34 | 35 | fun replyError(code: String, message: String? = null, obj: Any? = null) { 36 | if (isReplied) { 37 | return 38 | } 39 | isReplied = true 40 | val result = this.result 41 | handler.post { 42 | result.error(code, message, obj) 43 | } 44 | } 45 | 46 | fun notImplemented() { 47 | if (isReplied) { 48 | return 49 | } 50 | isReplied = true 51 | val result = this.result 52 | handler.post { 53 | result.notImplemented() 54 | } 55 | } 56 | 57 | fun isReplied(): Boolean { 58 | return isReplied 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /copy_on_publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm macos 4 | cp -r ios macos 5 | git rm --cached macos -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | **/Podfile.lock 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | /pubspec.lock 35 | 36 | # Web related 37 | lib/generated_plugin_registrant.dart 38 | 39 | # Symbolication related 40 | app.*.symbols 41 | 42 | # Obfuscation related 43 | app.*.map.json 44 | .metadata 45 | 46 | local.properties 47 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # photo_manager_example 2 | 3 | Demonstrates how to use the photo_manager plugin. 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](https://flutter.io/). 9 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | avoid_print: false 6 | dangling_library_doc_comments: false 7 | use_build_context_synchronously: false 8 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .kotlin 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | id 'org.jetbrains.kotlin.kapt' 6 | } 7 | 8 | def localProperties = new Properties() 9 | def localPropertiesFile = rootProject.file('local.properties') 10 | if (localPropertiesFile.exists()) { 11 | localPropertiesFile.withReader('UTF-8') { reader -> 12 | localProperties.load(reader) 13 | } 14 | } 15 | 16 | android { 17 | namespace 'com.fluttercandies.photo_manager_example' 18 | 19 | compileSdk flutter.compileSdkVersion 20 | ndkVersion = "27.0.12077973" 21 | 22 | kotlinOptions { 23 | jvmTarget = '17' 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_17 28 | targetCompatibility JavaVersion.VERSION_17 29 | } 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | applicationId "com.fluttercandies.photo_manager_example" 37 | minSdkVersion 21 38 | targetSdk = flutter.targetSdkVersion 39 | versionCode = flutter.versionCode 40 | versionName = flutter.versionName 41 | } 42 | 43 | signingConfigs { 44 | forAll { 45 | storeFile file("${rootDir.absolutePath}/photo-manager-keystore") 46 | storePassword '123456' 47 | keyAlias 'key0' 48 | keyPassword '123456' 49 | } 50 | } 51 | 52 | buildTypes { 53 | debug { 54 | signingConfig signingConfigs.forAll 55 | } 56 | profile { 57 | signingConfig signingConfigs.forAll 58 | } 59 | release { 60 | signingConfig signingConfigs.forAll 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation 'com.github.bumptech.glide:glide:4.15.1' 71 | kapt 'com.github.bumptech.glide:compiler:4.15.1' 72 | } 73 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 19 | 27 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/fluttercandies/photo_manager_example/ExampleAppGlideModule.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager_example 2 | 3 | import com.bumptech.glide.module.AppGlideModule 4 | 5 | @com.bumptech.glide.annotation.GlideModule 6 | class ExampleAppGlideModule : AppGlideModule() -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/fluttercandies/photo_manager_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.photo_manager_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlin.code.style=official 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /example/android/photo-manager-keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/android/photo-manager-keystore -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.9.1" apply false 22 | id "com.android.library" version "8.9.1" apply false 23 | id "org.jetbrains.kotlin.android" version "2.1.20" apply false 24 | id "org.jetbrains.kotlin.kapt" version "2.1.20" apply false 25 | } 26 | 27 | include ":app" 28 | -------------------------------------------------------------------------------- /example/custom.dart: -------------------------------------------------------------------------------- 1 | import 'package:photo_manager/photo_manager.dart'; 2 | 3 | void main(List args) { 4 | final filter = AdvancedCustomFilter().addWhereCondition( 5 | WhereConditionGroup() 6 | .andGroup( 7 | WhereConditionGroup() 8 | .andText('width > 1000') 9 | .andText('height > 1000'), 10 | ) 11 | .orGroup( 12 | WhereConditionGroup().andText('width < 500').andText('height < 500'), 13 | ), 14 | ); 15 | 16 | PhotoManager.getAssetPathList(filterOption: filter).then((value) { 17 | print(value); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /example/devtools_options.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Flutter/ephemeral/ 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | .build/ -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | photo_manager Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | com.fluttercandies.photo_manager_example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | 32 | NSBonjourServices 33 | 34 | _dartobservatory._tcp 35 | 36 | NSLocalNetworkUsageDescription 37 | Requesting local network for debugging processes 38 | NSPhotoLibraryUsageDescription 39 | You must agree with the photo album to see your pictures 40 | NSPhotoLibraryAddUsageDescription 41 | You must agree the additions permission to save assets. 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | UIViewControllerBasedStatusBarAppearance 60 | 61 | CADisableMinimumFrameDurationOnPhone 62 | 63 | UIApplicationSupportsIndirectInputEvents 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/page/custom_filter/advance_filter_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import 'order_by_action.dart'; 5 | import 'where_action.dart'; 6 | 7 | class AdvancedCustomFilterPage extends StatefulWidget { 8 | const AdvancedCustomFilterPage({ 9 | super.key, 10 | required this.builder, 11 | }); 12 | 13 | final Widget Function(BuildContext context, CustomFilter filter) builder; 14 | 15 | @override 16 | State createState() => 17 | _AdvancedCustomFilterPageState(); 18 | } 19 | 20 | class _AdvancedCustomFilterPageState extends State { 21 | List _where = []; 22 | List _orderBy = [ 23 | OrderByItem.named( 24 | column: CustomColumns.base.createDate, 25 | isAsc: false, 26 | ), 27 | ]; 28 | 29 | late CustomFilter filter; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | filter = _createFilter(); 35 | } 36 | 37 | AdvancedCustomFilter _createFilter() { 38 | final filter = AdvancedCustomFilter( 39 | orderBy: _orderBy, 40 | where: _where, 41 | ); 42 | return filter; 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return Scaffold( 48 | appBar: AppBar( 49 | title: const Text('Advanced Custom Filter Example'), 50 | actions: [ 51 | WhereAction( 52 | items: _where, 53 | onChanged: (value) { 54 | if (!mounted) { 55 | return; 56 | } 57 | setState(() { 58 | _where = value; 59 | filter = _createFilter(); 60 | }); 61 | }, 62 | ), 63 | OrderByAction( 64 | items: _orderBy, 65 | onChanged: (values) { 66 | if (!mounted) { 67 | return; 68 | } 69 | setState(() { 70 | _orderBy = values; 71 | filter = _createFilter(); 72 | }); 73 | }, 74 | ), 75 | ], 76 | ), 77 | body: Column( 78 | children: [ 79 | Expanded( 80 | child: widget.builder(context, filter), 81 | ), 82 | ], 83 | ), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /example/lib/page/custom_filter/filter_assets_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | import 'package:photo_manager_example/page/custom_filter/image_list.dart'; 4 | 5 | class FilterAssetsContent extends StatelessWidget { 6 | const FilterAssetsContent({ 7 | super.key, 8 | required this.filter, 9 | }); 10 | final CustomFilter filter; 11 | 12 | Future> getAssets() async { 13 | final count = await PhotoManager.getAssetCount(filterOption: filter); 14 | if (count == 0) { 15 | return []; 16 | } 17 | final list = await PhotoManager.getAssetListRange( 18 | start: 0, 19 | end: count, 20 | filterOption: filter, 21 | ); 22 | return list; 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return FutureBuilder>( 28 | future: getAssets(), 29 | builder: 30 | (BuildContext context, AsyncSnapshot> snapshot) { 31 | if (snapshot.hasData) { 32 | return ImageList(list: snapshot.data!); 33 | } 34 | return const SizedBox(); 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/lib/page/custom_filter/image_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | import 'package:photo_manager_example/widget/image_item_widget.dart'; 4 | 5 | class ImageList extends StatelessWidget { 6 | const ImageList({super.key, required this.list}); 7 | 8 | final List list; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return GridView.builder( 13 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 14 | crossAxisCount: 5, 15 | mainAxisSpacing: 2, 16 | crossAxisSpacing: 2, 17 | ), 18 | itemBuilder: (context, index) { 19 | final entity = list[index]; 20 | return ImageItemWidget( 21 | entity: entity, 22 | option: ThumbnailOption.ios( 23 | size: const ThumbnailSize.square(500), 24 | ), 25 | ); 26 | }, 27 | itemCount: list.length, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/lib/page/custom_filter/include_hidden_test_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | class IncludeHiddenTestPage extends StatefulWidget { 5 | const IncludeHiddenTestPage({super.key}); 6 | 7 | @override 8 | State createState() => _IncludeHiddenTestPageState(); 9 | } 10 | 11 | class _IncludeHiddenTestPageState extends State { 12 | final List _logs = []; 13 | 14 | Future _test() async { 15 | final option = CustomFilter.sql(where: ''); 16 | final count = await PhotoManager.getAssetCount(filterOption: option); 17 | 18 | setState(() { 19 | _logs.add('Not include hidden count: $count'); 20 | }); 21 | 22 | final option2 = CustomFilter.sql(where: ''); 23 | option2.includeHiddenAssets = true; 24 | final count2 = await PhotoManager.getAssetCount(filterOption: option2); 25 | 26 | setState(() { 27 | _logs.add('Include hidden count: $count2'); 28 | }); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Scaffold( 34 | appBar: AppBar( 35 | title: const Text('Include Hidden Test'), 36 | ), 37 | body: Column( 38 | children: [ 39 | ElevatedButton( 40 | onPressed: _test, 41 | child: const Text('Test the count of hidden and not hidden'), 42 | ), 43 | Expanded( 44 | child: ListView( 45 | children: [ 46 | ..._logs.reversed.map((e) => ListTile(title: Text(e))), 47 | ], 48 | ), 49 | ), 50 | ], 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/lib/page/custom_filter/path_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:oktoast/oktoast.dart'; 3 | import 'package:photo_manager/photo_manager.dart'; 4 | import 'package:photo_manager_example/page/custom_filter/path_page.dart'; 5 | 6 | class FilterPathList extends StatelessWidget { 7 | const FilterPathList({super.key, required this.filter}); 8 | 9 | final CustomFilter filter; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return FutureBuilder>( 14 | future: Future(() async { 15 | final ps = await PhotoManager.requestPermissionExtend(); 16 | if (!ps.hasAccess) { 17 | throw StateError('No access'); 18 | } 19 | return PhotoManager.getAssetPathList(filterOption: filter); 20 | }), 21 | builder: ( 22 | BuildContext context, 23 | AsyncSnapshot> snapshot, 24 | ) { 25 | if (snapshot.hasData) { 26 | return PathList(list: snapshot.data!); 27 | } 28 | return const SizedBox(); 29 | }, 30 | ); 31 | } 32 | } 33 | 34 | class PathList extends StatelessWidget { 35 | const PathList({ 36 | super.key, 37 | required this.list, 38 | }); 39 | 40 | final List list; 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return ListView.builder( 45 | itemBuilder: (BuildContext context, int index) { 46 | final AssetPathEntity path = list[index]; 47 | return ListTile( 48 | title: Text(path.name), 49 | subtitle: Text(path.id), 50 | trailing: FutureBuilder( 51 | future: path.assetCountAsync, 52 | builder: (BuildContext context, AsyncSnapshot snapshot) { 53 | if (snapshot.hasData) { 54 | return Text(snapshot.data.toString()); 55 | } 56 | return const SizedBox(); 57 | }, 58 | ), 59 | onTap: () { 60 | path.assetCountAsync.then((value) { 61 | showToast( 62 | 'Asset count: $value', 63 | position: ToastPosition.bottom, 64 | ); 65 | }); 66 | Navigator.push( 67 | context, 68 | MaterialPageRoute( 69 | builder: (_) => PathPage(path: path), 70 | ), 71 | ); 72 | }, 73 | ); 74 | }, 75 | itemCount: list.length, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /example/lib/page/custom_filter/path_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | import 'package:photo_manager_example/page/custom_filter/image_list.dart'; 4 | 5 | class PathPage extends StatefulWidget { 6 | const PathPage({super.key, required this.path}); 7 | final AssetPathEntity path; 8 | 9 | @override 10 | State createState() => _PathPageState(); 11 | } 12 | 13 | class _PathPageState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: Text(widget.path.name), 19 | ), 20 | body: GalleryWidget( 21 | path: widget.path, 22 | ), 23 | ); 24 | } 25 | } 26 | 27 | class GalleryWidget extends StatefulWidget { 28 | const GalleryWidget({super.key, required this.path}); 29 | 30 | final AssetPathEntity path; 31 | 32 | @override 33 | State createState() => _GalleryWidgetState(); 34 | } 35 | 36 | class _GalleryWidgetState extends State { 37 | List _list = []; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | _refresh(); 43 | } 44 | 45 | Future _refresh() async { 46 | final count = await widget.path.assetCountAsync; 47 | if (count == 0) { 48 | return; 49 | } 50 | final list = await widget.path.getAssetListRange(start: 0, end: count); 51 | setState(() { 52 | if (mounted) { 53 | _list = list; 54 | } 55 | }); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return ImageList(list: _list); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/lib/page/developer/android/column_names_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:photo_manager/photo_manager.dart'; 4 | 5 | class ColumnNamesPage extends StatefulWidget { 6 | const ColumnNamesPage({super.key}); 7 | 8 | @override 9 | State createState() => _ColumnNamesPageState(); 10 | } 11 | 12 | class _ColumnNamesPageState extends State { 13 | List _columns = []; 14 | 15 | Future _refresh() async { 16 | final columns = await PhotoManager.plugin.androidColumns(); 17 | print('columns: $columns'); 18 | columns.sort(); 19 | setState(() { 20 | _columns = columns; 21 | }); 22 | } 23 | 24 | @override 25 | void initState() { 26 | _refresh(); 27 | super.initState(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | appBar: AppBar( 34 | title: const Text('Column Names'), 35 | actions: [ 36 | IconButton( 37 | icon: const Icon(Icons.refresh), 38 | onPressed: _refresh, 39 | ), 40 | ], 41 | ), 42 | body: _buildBody(), 43 | ); 44 | } 45 | 46 | Widget _buildBody() { 47 | final list = _columns; 48 | return ListView.builder( 49 | itemBuilder: (context, index) { 50 | final column = list[index]; 51 | return ListTile( 52 | title: Text(column), 53 | subtitle: const Text('click to copy'), 54 | onTap: () { 55 | Clipboard.setData(ClipboardData(text: column)); 56 | }, 57 | ); 58 | }, 59 | itemCount: list.length, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/lib/page/developer/create_entity_by_id.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:oktoast/oktoast.dart'; 3 | import 'package:photo_manager/photo_manager.dart'; 4 | 5 | import '../detail_page.dart'; 6 | 7 | class CreateEntityById extends StatefulWidget { 8 | const CreateEntityById({super.key}); 9 | 10 | @override 11 | State createState() => _CreateEntityByIdState(); 12 | } 13 | 14 | class _CreateEntityByIdState extends State { 15 | TextEditingController controller = TextEditingController(); 16 | 17 | @override 18 | void initState() { 19 | super.initState(); 20 | controller.text = '1016711'; 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: const Text('Create AssetEntity by id'), 28 | ), 29 | body: Padding( 30 | padding: const EdgeInsets.all(8.0), 31 | child: Column( 32 | children: [ 33 | TextField( 34 | controller: controller, 35 | decoration: const InputDecoration( 36 | hintText: 'input asset id', 37 | ), 38 | ), 39 | ElevatedButton( 40 | onPressed: createAssetEntityAndShow, 41 | child: const Text('Create assetEntity'), 42 | ), 43 | ], 44 | ), 45 | ), 46 | ); 47 | } 48 | 49 | Future createAssetEntityAndShow() async { 50 | final String id = controller.text.trim(); 51 | final AssetEntity? asset = await AssetEntity.fromId(id); 52 | if (asset == null) { 53 | showToast('Cannot create asset by $id'); 54 | return; 55 | } 56 | 57 | if (!mounted) { 58 | return; 59 | } 60 | return Navigator.push( 61 | context, 62 | MaterialPageRoute( 63 | builder: (BuildContext c) => DetailPage(entity: asset), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/lib/page/developer/dev_title_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import '../../util/log.dart'; 5 | 6 | class DevelopingExample extends StatefulWidget { 7 | const DevelopingExample({super.key}); 8 | 9 | @override 10 | State createState() => _DevelopingExampleState(); 11 | } 12 | 13 | class _DevelopingExampleState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar(), 18 | body: Container( 19 | padding: const EdgeInsets.all(8.0), 20 | alignment: Alignment.topCenter, 21 | child: ElevatedButton( 22 | child: const Text('Test title speed'), 23 | onPressed: () async { 24 | final DateTime start = DateTime.now(); 25 | final PermissionState result = 26 | await PhotoManager.requestPermissionExtend(); 27 | if (result.isAuth) { 28 | final List imageList = []; 29 | final List list = 30 | await PhotoManager.getAssetPathList( 31 | type: RequestType.image, 32 | ); 33 | for (final AssetPathEntity path in list) { 34 | imageList.addAll( 35 | await path.getAssetListRange(start: 0, end: 1), 36 | ); 37 | } 38 | if (imageList.isNotEmpty) { 39 | imageList.shuffle(); 40 | } 41 | } 42 | final Duration diff = DateTime.now().difference(start); 43 | Log.d(diff); 44 | }, 45 | ), 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_1025.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import 'issue_index_page.dart'; 5 | 6 | class Issue1025Page extends StatefulWidget { 7 | const Issue1025Page({super.key}); 8 | 9 | @override 10 | State createState() => _Issue1025PageState(); 11 | } 12 | 13 | class _Issue1025PageState extends State 14 | with IssueBase { 15 | String log = ''; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return buildScaffold( 20 | [ 21 | buildButton('Test for ignore permission', _testForIgnorePermission), 22 | Expanded(child: Text(log)), 23 | ], 24 | ); 25 | } 26 | 27 | @override 28 | int get issueNumber => 1025; 29 | 30 | Future _testForIgnorePermission() async { 31 | await PhotoManager.setIgnorePermissionCheck(true); 32 | setState(() { 33 | log = 'setIgnorePermissionCheck(true) success' '\n$log'; 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_1031.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import 'issue_index_page.dart'; 5 | 6 | class Issue1031Page extends StatefulWidget { 7 | const Issue1031Page({super.key}); 8 | 9 | @override 10 | State createState() => _Issue1031PageState(); 11 | } 12 | 13 | class _Issue1031PageState extends State 14 | with IssueBase { 15 | String log = ''; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return buildScaffold( 20 | [ 21 | buildButton('Test for permission', _testForIgnorePermission), 22 | Expanded(child: Text(log)), 23 | ], 24 | ); 25 | } 26 | 27 | @override 28 | int get issueNumber => 1031; 29 | 30 | Future _testForIgnorePermission() async { 31 | final permission = await PhotoManager.requestPermissionExtend(); 32 | log = 'permission: $permission' '\n$log'; 33 | log = 'isAuth: ${permission.isAuth}' '\n$log'; 34 | setState(() {}); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_1051.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | import 'package:photo_manager_example/page/developer/issues_page/issue_index_page.dart'; 4 | 5 | class Issus1051 extends StatefulWidget { 6 | const Issus1051({super.key}); 7 | 8 | @override 9 | State createState() => _Issus1051State(); 10 | } 11 | 12 | class _Issus1051State extends State with IssueBase { 13 | Future _test() async { 14 | final status = await PhotoManager.requestPermissionExtend(); 15 | if (status.hasAccess) { 16 | await PhotoManager.presentLimited(); 17 | print('present limited'); 18 | } 19 | 20 | print('status: $status'); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return buildScaffold([ 26 | buildButton( 27 | 'Test presentLimited', 28 | _test, 29 | ), 30 | ]); 31 | } 32 | 33 | @override 34 | int get issueNumber => 1051; 35 | 36 | @override 37 | List? get supportPlatforms => [ 38 | TargetPlatform.android, 39 | ]; 40 | } 41 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_1053.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:oktoast/oktoast.dart'; 3 | import 'package:photo_manager/photo_manager.dart'; 4 | 5 | import 'issue_index_page.dart'; 6 | 7 | class Issus1053 extends StatefulWidget { 8 | const Issus1053({super.key}); 9 | 10 | @override 11 | State createState() => _Issus1053State(); 12 | } 13 | 14 | class _Issus1053State extends State with IssueBase { 15 | Future _requestPermission() async { 16 | if (checkStatus.isEmpty) { 17 | showToast('Please select at least one type'); 18 | return; 19 | } 20 | 21 | final requestType = RequestType.fromTypes(checkStatus); 22 | 23 | final PermissionState status = await PhotoManager.requestPermissionExtend( 24 | requestOption: PermissionRequestOption( 25 | androidPermission: AndroidPermission( 26 | type: requestType, 27 | mediaLocation: false, 28 | ), 29 | ), 30 | ); 31 | addLog('status: $status'); 32 | } 33 | 34 | @override 35 | List? get supportPlatforms => [ 36 | TargetPlatform.android, 37 | ]; 38 | 39 | List checkStatus = []; 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return buildScaffold([ 44 | for (final item in RequestType.values) _buildCheck(item), 45 | buildButton( 46 | 'Test requestPermissionExtend', 47 | _requestPermission, 48 | ), 49 | buildLogWidget(), 50 | ]); 51 | } 52 | 53 | @override 54 | int get issueNumber => 1053; 55 | 56 | Widget _buildCheck(RequestType item) { 57 | String title = ''; 58 | switch (item.value) { 59 | case 1: 60 | title = 'image'; 61 | break; 62 | case 2: 63 | title = 'video'; 64 | break; 65 | case 4: 66 | title = 'audio'; 67 | break; 68 | } 69 | 70 | return CheckboxListTile( 71 | title: Text(title), 72 | value: checkStatus.contains(item), 73 | onChanged: (checked) { 74 | if (checked == true) { 75 | checkStatus.add(item); 76 | } else { 77 | checkStatus.remove(item); 78 | } 79 | setState(() {}); 80 | }, 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_1152.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:oktoast/oktoast.dart'; 3 | import 'package:photo_manager/photo_manager.dart'; 4 | 5 | import 'issue_index_page.dart'; 6 | 7 | class Issus1152 extends StatefulWidget { 8 | const Issus1152({super.key}); 9 | 10 | @override 11 | State createState() => _Issus1152State(); 12 | } 13 | 14 | class _Issus1152State extends State with IssueBase { 15 | @override 16 | List? get supportPlatforms => [ 17 | TargetPlatform.android, 18 | ]; 19 | 20 | List checkStatus = []; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | PhotoManager.setLog(true); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return buildScaffold([ 31 | buildButton( 32 | 'Test API 28 get images', 33 | reproduce, 34 | ), 35 | buildLogWidget(), 36 | ]); 37 | } 38 | 39 | @override 40 | int get issueNumber => 1152; 41 | 42 | Future reproduce() async { 43 | final pathList = await PhotoManager.getAssetPathList(); 44 | final noAllPathList = pathList.where((element) => !element.isAll); 45 | if (noAllPathList.isEmpty) { 46 | showToast('No path found'); 47 | return; 48 | } 49 | 50 | final path = noAllPathList.first; 51 | 52 | // final name = path.name; 53 | // final count = await path.assetCountAsync; 54 | // addLog('assetCount of "$name": $count'); 55 | 56 | // final list = await path.getAssetListPaged(page: 0, size: 50); 57 | // addLog('list.length: ${list.length}'); 58 | 59 | await fetchNewProperties(path); 60 | } 61 | 62 | Future fetchNewProperties(AssetPathEntity path) async { 63 | try { 64 | await path.obtainForNewProperties(); 65 | } catch (e) { 66 | addLog('error: $e'); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_734.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import '../../../util/log.dart'; 5 | import 'issue_index_page.dart'; 6 | 7 | class Issue734Page extends StatefulWidget { 8 | const Issue734Page({super.key}); 9 | 10 | @override 11 | State createState() => _Issue734PageState(); 12 | } 13 | 14 | class _Issue734PageState extends State 15 | with IssueBase { 16 | @override 17 | int get issueNumber => 734; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return buildScaffold( 22 | [ 23 | buildButton('Reproduct', _reproduct), 24 | ], 25 | ); 26 | } 27 | 28 | Future _reproduct() async { 29 | const FilterOption opt = FilterOption( 30 | needTitle: true, 31 | sizeConstraint: SizeConstraint(ignoreSize: true), 32 | ); 33 | 34 | final FilterOptionGroup group = FilterOptionGroup( 35 | imageOption: opt, 36 | audioOption: opt, 37 | videoOption: opt, 38 | ); 39 | 40 | final List pathList = 41 | await PhotoManager.getAssetPathList(filterOption: group); 42 | 43 | if (pathList.isEmpty) { 44 | Log.d('The path list is empty'); 45 | return; 46 | } 47 | 48 | final AssetPathEntity recent = pathList[0]; 49 | final int assetCount = await recent.assetCountAsync; 50 | Log.d('recent.assetCount: $assetCount'); 51 | 52 | for (int i = 0; i < assetCount; i++) { 53 | final List pageAssetList = await recent.getAssetListPaged( 54 | page: i, 55 | size: 1, 56 | ); 57 | 58 | try { 59 | Log.d(' page($i, 1) list asset count: ${pageAssetList.length}'); 60 | Log.d(' page($i, 1) list asset[0] id: ${pageAssetList[0].id}'); 61 | } catch (e) { 62 | Log.d(e); 63 | } 64 | 65 | final List rangeList = 66 | await recent.getAssetListRange(start: i, end: i + 1); 67 | try { 68 | Log.d('range($i, ${i + 1}) list asset count: ${rangeList.length}'); 69 | Log.d('range($i, ${i + 1}) list asset[0] id: ${rangeList[0].id}'); 70 | } catch (e) { 71 | Log.d(e); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_918.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | import 'issue_index_page.dart'; 4 | 5 | class Issue918Page extends StatefulWidget { 6 | const Issue918Page({super.key}); 7 | 8 | @override 9 | State createState() => _Issue918PageState(); 10 | } 11 | 12 | class _Issue918PageState extends State 13 | with IssueBase { 14 | @override 15 | int get issueNumber => 918; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return buildScaffold( 20 | [ 21 | buildButton('Reproduct', _reproduct), 22 | ], 23 | ); 24 | } 25 | 26 | Future _reproduct() async { 27 | final assetList = await PhotoManager.getAssetListRange( 28 | start: 0, 29 | end: 10, 30 | filterOption: FilterOptionGroup( 31 | imageOption: const FilterOption(needTitle: true), 32 | videoOption: const FilterOption(needTitle: true), 33 | orders: [ 34 | const OrderOption(type: OrderOptionType.createDate), 35 | ], 36 | ), 37 | ); 38 | 39 | for (final asset in assetList) { 40 | print(asset.title); 41 | print(asset.createDateTime); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_962.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import 'issue_index_page.dart'; 5 | 6 | class Issue962 extends StatefulWidget { 7 | const Issue962({super.key}); 8 | 9 | @override 10 | State createState() => _Issue962State(); 11 | } 12 | 13 | class _Issue962State extends State with IssueBase { 14 | int start = 0; 15 | int end = 5; 16 | 17 | ValueNotifier logNotifier = ValueNotifier(''); 18 | 19 | Future onChange() async { 20 | if (start >= end) { 21 | logNotifier.value = 'start must less than end'; 22 | return; 23 | } 24 | 25 | final assetList = await PhotoManager.getAssetListRange( 26 | start: start, 27 | end: end, 28 | filterOption: FilterOptionGroup( 29 | imageOption: const FilterOption(needTitle: true), 30 | videoOption: const FilterOption(needTitle: true), 31 | orders: [ 32 | const OrderOption(type: OrderOptionType.createDate), 33 | ], 34 | ), 35 | ); 36 | 37 | logNotifier.value = 'The assetList count: ${assetList.length}'; 38 | } 39 | 40 | final node = FocusScopeNode(); 41 | 42 | @override 43 | void dispose() { 44 | node.dispose(); 45 | super.dispose(); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return buildScaffold([ 51 | TextFormField( 52 | initialValue: start.toString(), 53 | decoration: const InputDecoration( 54 | labelText: 'start', 55 | ), 56 | onChanged: (value) { 57 | start = int.tryParse(value) ?? 0; 58 | onChange(); 59 | }, 60 | ), 61 | TextFormField( 62 | initialValue: end.toString(), 63 | decoration: const InputDecoration( 64 | labelText: 'end', 65 | ), 66 | onChanged: (value) { 67 | end = int.tryParse(value) ?? 0; 68 | onChange(); 69 | }, 70 | ), 71 | buildButton('Reproduct issue 962', onChange), 72 | AnimatedBuilder( 73 | animation: logNotifier, 74 | builder: (context, w) { 75 | return Text( 76 | logNotifier.value, 77 | style: const TextStyle( 78 | fontSize: 12, 79 | ), 80 | ); 81 | }, 82 | ), 83 | ]); 84 | } 85 | 86 | @override 87 | int get issueNumber => 962; 88 | } 89 | -------------------------------------------------------------------------------- /example/lib/page/developer/issues_page/issue_979.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | import 'package:photo_manager_example/page/developer/issues_page/issue_index_page.dart'; 4 | import 'package:photo_manager_example/util/asset_utils.dart'; 5 | 6 | class Issue979 extends StatefulWidget { 7 | const Issue979({super.key}); 8 | 9 | @override 10 | State createState() => _Issue979State(); 11 | } 12 | 13 | class _Issue979State extends State with IssueBase { 14 | @override 15 | Widget build(BuildContext context) { 16 | return buildScaffold([ 17 | buildButton('Save image and read asset', _saveAndRead), 18 | buildLogWidget(), 19 | ]); 20 | } 21 | 22 | @override 23 | int get issueNumber => 979; 24 | 25 | Future _saveAndRead() async { 26 | final auth = await PhotoManager.requestPermissionExtend( 27 | requestOption: const PermissionRequestOption( 28 | iosAccessLevel: IosAccessLevel.addOnly, 29 | ), 30 | ); 31 | if (!auth.hasAccess) { 32 | addLog('request permission fail, $auth'); 33 | return; 34 | } 35 | 36 | addLog('request permission success, wait download file...'); 37 | 38 | try { 39 | final file = await AssetsUtils.downloadJpeg(); 40 | final name = file.path.split('/').last; 41 | final asset = await PhotoManager.editor.saveImageWithPath( 42 | file.path, 43 | title: name, 44 | ); 45 | final id = asset.id; 46 | final width = asset.width; 47 | final height = asset.height; 48 | addLog('The save id = $id, width = $width, height = $height'); 49 | } catch (e) { 50 | addLog('Save error : $e'); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/lib/page/developer/remove_all_android_not_exists_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | class RemoveAndroidNotExistsExample extends StatefulWidget { 5 | const RemoveAndroidNotExistsExample({super.key}); 6 | 7 | @override 8 | State createState() => 9 | _RemoveAndroidNotExistsExampleState(); 10 | } 11 | 12 | class _RemoveAndroidNotExistsExampleState 13 | extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: const Text('Remove android not exists assets.'), 19 | ), 20 | body: Padding( 21 | padding: const EdgeInsets.all(8.0), 22 | child: Column( 23 | crossAxisAlignment: CrossAxisAlignment.stretch, 24 | children: [ 25 | ElevatedButton( 26 | child: const Text('Click and see android logcat log.'), 27 | onPressed: () { 28 | PhotoManager.editor.android.removeAllNoExistsAsset(); 29 | }, 30 | ), 31 | ], 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/lib/page/gallery_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | import '../model/photo_provider.dart'; 6 | import '../widget/gallery_item_widget.dart'; 7 | 8 | class GalleryListPage extends StatefulWidget { 9 | const GalleryListPage({super.key}); 10 | 11 | @override 12 | State createState() => _GalleryListPageState(); 13 | } 14 | 15 | class _GalleryListPageState extends State { 16 | PhotoProvider get provider => context.watch(); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | appBar: AppBar( 22 | title: const Text('Gallery list'), 23 | ), 24 | body: Scrollbar( 25 | child: ListView.builder( 26 | itemBuilder: _buildItem, 27 | itemCount: provider.list.length, 28 | ), 29 | ), 30 | ); 31 | } 32 | 33 | Widget _buildItem(BuildContext context, int index) { 34 | final AssetPathEntity item = provider.list[index]; 35 | return GalleryItemWidget( 36 | path: item, 37 | setState: setState, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/lib/page/index_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager_example/page/custom_filter_example_page.dart'; 3 | import 'package:photo_manager_example/widget/nav_button.dart'; 4 | 5 | import 'change_notify_page.dart'; 6 | import 'developer/develop_index_page.dart'; 7 | import 'home_page.dart'; 8 | import 'save_image_example.dart'; 9 | 10 | class IndexPage extends StatefulWidget { 11 | const IndexPage({super.key}); 12 | 13 | @override 14 | State createState() => _IndexPageState(); 15 | } 16 | 17 | class _IndexPageState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | appBar: AppBar( 22 | title: const Text('Example for photo manager.'), 23 | ), 24 | body: ListView( 25 | padding: const EdgeInsets.all(8.0), 26 | children: [ 27 | routePage('Gallery list', const NewHomePage()), 28 | routePage('Custom filter example', const CustomFilterExamplePage()), 29 | routePage('Save media example', const SaveMediaExample()), 30 | routePage('Change notify example', const ChangeNotifyExample()), 31 | routePage('For Developer page', const DeveloperIndexPage()), 32 | ], 33 | ), 34 | ); 35 | } 36 | 37 | Widget routePage(String title, Widget page) { 38 | return NavButton(title: title, page: page); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/lib/page/sub_gallery_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import '../widget/gallery_item_widget.dart'; 5 | 6 | class SubFolderPage extends StatefulWidget { 7 | const SubFolderPage({ 8 | super.key, 9 | required this.pathList, 10 | required this.title, 11 | }); 12 | 13 | final List pathList; 14 | final String title; 15 | 16 | @override 17 | State createState() => _SubFolderPageState(); 18 | } 19 | 20 | class _SubFolderPageState extends State { 21 | @override 22 | Widget build(BuildContext context) { 23 | return Scaffold( 24 | appBar: AppBar( 25 | title: Text(widget.title), 26 | ), 27 | body: ListView.builder( 28 | itemBuilder: _buildItem, 29 | itemCount: widget.pathList.length, 30 | ), 31 | ); 32 | } 33 | 34 | Widget _buildItem(BuildContext context, int index) { 35 | final AssetPathEntity item = widget.pathList[index]; 36 | return GalleryItemWidget( 37 | path: item, 38 | setState: setState, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/lib/util/asset_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:http/http.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | 6 | class AssetsUtils { 7 | AssetsUtils._(); 8 | 9 | static const String jpegUrl = 10 | 'https://gitlab.com/CaiJingLong/ExampleAsset/-/raw/main/IMG_1096.jpeg?ref_type=heads'; 11 | 12 | static Future downloadJpeg() async { 13 | final cacheDir = await getTemporaryDirectory(); 14 | 15 | final dtMs = DateTime.now().millisecondsSinceEpoch; 16 | final file = File('${cacheDir.path}/$dtMs.jpg'); 17 | 18 | final bytes = await get(Uri.parse(jpegUrl)); 19 | await file.writeAsBytes(bytes.bodyBytes); 20 | return file; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/lib/util/log_export.dart: -------------------------------------------------------------------------------- 1 | import 'package:path_provider/path_provider.dart'; 2 | 3 | /// Because the photo_manager plugin does not depend on the path_provider plugin, 4 | /// we need to create a new class in the example to get the log file path. 5 | class PMVerboseLogUtil { 6 | PMVerboseLogUtil(); 7 | 8 | static final shared = PMVerboseLogUtil(); 9 | 10 | static String? _logDirPath; 11 | 12 | String _logFilePath = ''; 13 | 14 | /// Get the log file path. 15 | /// 16 | /// Use in the `PhotoManager.setLog` method. 17 | Future getLogFilePath() async { 18 | if (_logFilePath.isNotEmpty) { 19 | return _logFilePath; 20 | } 21 | 22 | if (_logDirPath == null) { 23 | final cacheDir = await getApplicationCacheDirectory(); 24 | _logDirPath = cacheDir.path; 25 | } 26 | 27 | final timeStr = DateTime.now().toIso8601String(); 28 | _logFilePath = '$_logDirPath/pmlog-$timeStr.txt'; 29 | 30 | return _logFilePath; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/lib/widget/change_notifier_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef ChangeNotifierWidgetBuilder = Widget Function( 4 | BuildContext context, 5 | T value, 6 | ); 7 | 8 | class ChangeNotifierBuilder extends StatefulWidget { 9 | const ChangeNotifierBuilder({ 10 | super.key, 11 | required this.builder, 12 | required this.value, 13 | }); 14 | 15 | final ChangeNotifierWidgetBuilder builder; 16 | final T value; 17 | 18 | @override 19 | State> createState() => 20 | _ChangeNotifierBuilderState(); 21 | } 22 | 23 | class _ChangeNotifierBuilderState 24 | extends State> { 25 | @override 26 | void initState() { 27 | super.initState(); 28 | widget.value.addListener(_onChange); 29 | } 30 | 31 | void _onChange() { 32 | if (mounted) { 33 | setState(() {}); 34 | } 35 | } 36 | 37 | @override 38 | void dispose() { 39 | widget.value.removeListener(_onChange); 40 | super.dispose(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return widget.builder(context, widget.value); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/lib/widget/dialog/list_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ListDialog extends StatefulWidget { 4 | const ListDialog({ 5 | super.key, 6 | required this.children, 7 | this.padding = EdgeInsets.zero, 8 | }); 9 | 10 | final List children; 11 | final EdgeInsetsGeometry padding; 12 | 13 | @override 14 | State createState() => _ListDialogState(); 15 | } 16 | 17 | class _ListDialogState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return Center( 21 | child: ListView( 22 | padding: widget.padding, 23 | shrinkWrap: true, 24 | children: widget.children, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/lib/widget/loading_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | final Center loadWidget = Center( 7 | child: SizedBox.fromSize( 8 | size: const Size.square(30), 9 | child: (Platform.isIOS || Platform.isMacOS) 10 | ? const CupertinoActivityIndicator() 11 | : const CircularProgressIndicator(), 12 | ), 13 | ); 14 | -------------------------------------------------------------------------------- /example/lib/widget/nav_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NavButton extends StatelessWidget { 4 | const NavButton({ 5 | super.key, 6 | required this.title, 7 | required this.page, 8 | }); 9 | 10 | final String title; 11 | final Widget page; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return CustomButton( 16 | onPressed: () => Navigator.push( 17 | context, 18 | MaterialPageRoute(builder: (_) => page), 19 | ), 20 | title: title, 21 | ); 22 | } 23 | } 24 | 25 | class CustomButton extends StatelessWidget { 26 | const CustomButton({ 27 | super.key, 28 | required this.title, 29 | required this.onPressed, 30 | }); 31 | 32 | final String title; 33 | final VoidCallback onPressed; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Container( 38 | padding: const EdgeInsets.all(8.0), 39 | height: 64, 40 | child: ElevatedButton( 41 | onPressed: onPressed, 42 | child: Text(title), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/lib/widget/nav_column.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager_example/widget/theme_button.dart'; 3 | 4 | String _defaultBuilder(Widget w) { 5 | return w.toStringShort(); 6 | } 7 | 8 | class NavColumn extends StatelessWidget { 9 | const NavColumn({ 10 | super.key, 11 | required this.children, 12 | this.titleBuilder = _defaultBuilder, 13 | }); 14 | 15 | final List children; 16 | final String Function(Widget w) titleBuilder; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Padding( 21 | padding: const EdgeInsets.all(8.0), 22 | child: SingleChildScrollView( 23 | child: Center( 24 | child: Column( 25 | children: [ 26 | for (final Widget item in children) buildItem(context, item), 27 | ], 28 | ), 29 | ), 30 | ), 31 | ); 32 | } 33 | 34 | Widget buildItem(BuildContext context, Widget item) { 35 | return ThemeButton( 36 | text: titleBuilder(item), 37 | onPressed: () { 38 | Navigator.push( 39 | context, 40 | MaterialPageRoute(builder: (_) => item), 41 | ); 42 | }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/lib/widget/theme_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ThemeButton extends StatelessWidget { 4 | const ThemeButton({ 5 | super.key, 6 | required this.text, 7 | required this.onPressed, 8 | }); 9 | 10 | final String text; 11 | final VoidCallback onPressed; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | height: 44, 17 | width: double.infinity, 18 | margin: const EdgeInsets.only(bottom: 5, top: 5), 19 | child: ElevatedButton( 20 | onPressed: onPressed, 21 | child: Text(text), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/xcuserdata/ 7 | 8 | Flutter/GeneratedPluginRegistrant.swift -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.15' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = photo_manager Example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.fluttercandies.photo-manager-example 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2021 FlutterCandies. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.movies.read-write 8 | 9 | com.apple.security.assets.music.read-write 10 | 11 | com.apple.security.assets.pictures.read-write 12 | 13 | com.apple.security.cs.allow-jit 14 | 15 | com.apple.security.network.server 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryUsageDescription 6 | I will use photo library 7 | NSPhotoLibraryAddUsageDescription 8 | You must agree the additions permission to save assets. 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIconFile 14 | 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | $(PRODUCT_NAME) 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(FLUTTER_BUILD_NAME) 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSHumanReadableCopyright 30 | $(PRODUCT_COPYRIGHT) 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.movies.read-write 8 | 9 | com.apple.security.assets.music.read-write 10 | 11 | com.apple.security.assets.pictures.read-write 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: photo_manager_example 2 | description: Demonstrates how to use the photo_manager plugin. 3 | publish_to: none 4 | 5 | environment: 6 | sdk: ">=2.19.0" 7 | flutter: ">=3.7.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | photo_manager: # override 14 | photo_manager_image_provider: ^2.1.1 15 | 16 | http: ">=0.13.6 <2.0.0" 17 | oktoast: ^3.1.0 18 | path_provider: ^2.0.5 19 | provider: ^6.0.0 20 | url_launcher: ^6.1.10 21 | video_player: ^2.1.14 22 | image: ^4.5.4 23 | 24 | dev_dependencies: 25 | flutter_lints: any 26 | flutter_test: 27 | sdk: flutter 28 | test: any 29 | 30 | flutter: 31 | uses-material-design: true 32 | 33 | dependency_overrides: 34 | photo_manager: 35 | path: ../ 36 | -------------------------------------------------------------------------------- /example_ohos/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | **/Podfile.lock 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | .metadata -------------------------------------------------------------------------------- /example_ohos/README.md: -------------------------------------------------------------------------------- 1 | # photo_manager_example 2 | 3 | Demonstrates how to use the photo_manager plugin on OpenHarmony. 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](https://flutter.io/). 9 | -------------------------------------------------------------------------------- /example_ohos/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | avoid_print: false 6 | dangling_library_doc_comments: false 7 | use_build_context_synchronously: false 8 | -------------------------------------------------------------------------------- /example_ohos/custom.dart: -------------------------------------------------------------------------------- 1 | import 'package:photo_manager/photo_manager.dart'; 2 | 3 | void main(List args) { 4 | final filter = AdvancedCustomFilter().addWhereCondition( 5 | WhereConditionGroup() 6 | .andGroup( 7 | WhereConditionGroup() 8 | .andText('width > 1000') 9 | .andText('height > 1000'), 10 | ) 11 | .orGroup( 12 | WhereConditionGroup().andText('width < 500').andText('height < 500'), 13 | ), 14 | ); 15 | 16 | PhotoManager.getAssetPathList(filterOption: filter).then((value) { 17 | print(value); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /example_ohos/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:photo_manager_example/main.dart' as example; 2 | 3 | void main() { 4 | example.main(); 5 | } 6 | -------------------------------------------------------------------------------- /example_ohos/ohos/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /local.properties 4 | /.idea 5 | **/build 6 | /.hvigor 7 | .cxx 8 | /.clangd 9 | /.clang-format 10 | /.clang-tidy 11 | **/.test 12 | *.har 13 | **/BuildProfile.ets 14 | **/oh-package-lock.json5 15 | 16 | **/src/main/resources/rawfile/flutter_assets/ 17 | **/libs/arm64-v8a/libapp.so 18 | **/libs/arm64-v8a/libflutter.so 19 | **/libs/arm64-v8a/libvmservice_snapshot.so 20 | -------------------------------------------------------------------------------- /example_ohos/ohos/AppScope/app.json5: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "bundleName": "com.fluttercandies.example", 4 | "vendor": "example", 5 | "versionCode": 1000000, 6 | "versionName": "1.0.0", 7 | "icon": "$media:app_icon", 8 | "label": "$string:app_name" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example_ohos/ohos/AppScope/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "app_name", 5 | "value": "example" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /example_ohos/ohos/AppScope/resources/base/media/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example_ohos/ohos/AppScope/resources/base/media/app_icon.png -------------------------------------------------------------------------------- /example_ohos/ohos/build-profile.json5: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "signingConfigs": [], 4 | "products": [ 5 | { 6 | "name": "default", 7 | "signingConfig": "default", 8 | "compileSdkVersion": "4.1.0(11)", 9 | "compatibleSdkVersion": "4.1.0(11)", 10 | "runtimeOS": "HarmonyOS" 11 | } 12 | ] 13 | }, 14 | "modules": [ 15 | { 16 | "name": "entry", 17 | "srcPath": "./entry", 18 | "targets": [ 19 | { 20 | "name": "default", 21 | "applyToProducts": [ 22 | "default" 23 | ] 24 | } 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /example_ohos/ohos/entry/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /node_modules 3 | /oh_modules 4 | /.preview 5 | /build 6 | /.cxx 7 | /.test 8 | /har 9 | /libs 10 | oh-package-lock.json5 -------------------------------------------------------------------------------- /example_ohos/ohos/entry/build-profile.json5: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | { 17 | "apiType": 'stageMode', 18 | "buildOption": { 19 | }, 20 | "targets": [ 21 | { 22 | "name": "default", 23 | "runtimeOS": "HarmonyOS" 24 | }, 25 | { 26 | "name": "ohosTest", 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /example_ohos/ohos/entry/hvigorfile.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | // Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. 17 | export { hapTasks } from '@ohos/hvigor-ohos-plugin'; 18 | -------------------------------------------------------------------------------- /example_ohos/ohos/entry/oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entry", 3 | "version": "1.0.0", 4 | "description": "Please describe the basic information.", 5 | "main": "", 6 | "author": "", 7 | "license": "", 8 | "dependencies": { 9 | "path_provider_ohos": "file:../har/path_provider_ohos.har", 10 | "url_launcher_ohos": "file:../har/url_launcher_ohos.har", 11 | "video_player_ohos": "file:../har/video_player_ohos.har", 12 | "photo_manager": "file:../har/photo_manager.har" 13 | } 14 | } -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/ets/entryability/EntryAbility.ets: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos'; 17 | import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant'; 18 | 19 | export default class EntryAbility extends FlutterAbility { 20 | configureFlutterEngine(flutterEngine: FlutterEngine) { 21 | super.configureFlutterEngine(flutterEngine) 22 | GeneratedPluginRegistrant.registerWith(flutterEngine) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/ets/pages/Index.ets: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import common from '@ohos.app.ability.common'; 17 | import { FlutterPage } from '@ohos/flutter_ohos' 18 | 19 | let storage = LocalStorage.getShared() 20 | const EVENT_BACK_PRESS = 'EVENT_BACK_PRESS' 21 | 22 | @Entry(storage) 23 | @Component 24 | struct Index { 25 | private context = getContext(this) as common.UIAbilityContext 26 | @LocalStorageLink('viewId') viewId: string = ""; 27 | 28 | build() { 29 | Column() { 30 | FlutterPage({ viewId: this.viewId }) 31 | } 32 | } 33 | 34 | onBackPress(): boolean { 35 | this.context.eventHub.emit(EVENT_BACK_PRESS) 36 | return true 37 | } 38 | } -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets: -------------------------------------------------------------------------------- 1 | import { FlutterEngine, Log } from '@ohos/flutter_ohos'; 2 | import PathProviderPlugin from 'path_provider_ohos'; 3 | import PhotoManagerPlugin from 'photo_manager'; 4 | import UrlLauncherPlugin from 'url_launcher_ohos'; 5 | import VideoPlayerPlugin from 'video_player_ohos'; 6 | 7 | /** 8 | * Generated file. Do not edit. 9 | * This file is generated by the Flutter tool based on the 10 | * plugins that support the Ohos platform. 11 | */ 12 | 13 | const TAG = "GeneratedPluginRegistrant"; 14 | 15 | export class GeneratedPluginRegistrant { 16 | 17 | static registerWith(flutterEngine: FlutterEngine) { 18 | try { 19 | flutterEngine.getPlugins()?.add(new PathProviderPlugin()); 20 | flutterEngine.getPlugins()?.add(new PhotoManagerPlugin()); 21 | flutterEngine.getPlugins()?.add(new UrlLauncherPlugin()); 22 | flutterEngine.getPlugins()?.add(new VideoPlayerPlugin()); 23 | } catch (e) { 24 | Log.e( 25 | TAG, 26 | "Tried to register plugins with FlutterEngine (" 27 | + flutterEngine 28 | + ") failed."); 29 | Log.e(TAG, "Received exception while registering", e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/module.json5: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | { 16 | "module": { 17 | "name": "entry", 18 | "type": "entry", 19 | "description": "$string:module_desc", 20 | "mainElement": "EntryAbility", 21 | "deviceTypes": [ 22 | "phone" 23 | ], 24 | "deliveryWithInstall": true, 25 | "installationFree": false, 26 | "pages": "$profile:main_pages", 27 | "abilities": [ 28 | { 29 | "name": "EntryAbility", 30 | "srcEntry": "./ets/entryability/EntryAbility.ets", 31 | "description": "$string:EntryAbility_desc", 32 | "icon": "$media:icon", 33 | "label": "$string:EntryAbility_label", 34 | "startWindowIcon": "$media:icon", 35 | "startWindowBackground": "$color:start_window_background", 36 | "exported": true, 37 | "skills": [ 38 | { 39 | "entities": [ 40 | "entity.system.home" 41 | ], 42 | "actions": [ 43 | "action.system.home" 44 | ] 45 | } 46 | ] 47 | } 48 | ], 49 | "requestPermissions": [ 50 | {"name" : "ohos.permission.INTERNET"}, 51 | { 52 | "name": "ohos.permission.READ_IMAGEVIDEO", 53 | "reason": "$string:EntryAbility_label", 54 | "usedScene": { 55 | "abilities": [ 56 | "EntryAbility" 57 | ], 58 | "when": "inuse" 59 | } 60 | }, 61 | { 62 | "name": "ohos.permission.WRITE_IMAGEVIDEO", 63 | "reason": "$string:EntryAbility_label", 64 | "usedScene": { 65 | "abilities": [ 66 | "EntryAbility" 67 | ], 68 | "when": "inuse" 69 | } 70 | } 71 | ] 72 | } 73 | } -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/resources/base/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#FFFFFF" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "module description" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "example" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/resources/base/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/example_ohos/ohos/entry/src/main/resources/base/media/icon.png -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/resources/base/profile/main_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": [ 3 | "pages/Index" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/resources/en_US/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "module description" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "example" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /example_ohos/ohos/entry/src/main/resources/zh_CN/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "模块描述" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "example" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /example_ohos/ohos/hvigor/hvigor-config.json5: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | { 17 | "hvigorVersion": "4.1.2", 18 | "dependencies": { 19 | "@ohos/hvigor-ohos-plugin": "4.1.2" 20 | } 21 | } -------------------------------------------------------------------------------- /example_ohos/ohos/hvigorfile.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import { appTasks } from '@ohos/hvigor-ohos-plugin'; 17 | 18 | export default { 19 | system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ 20 | plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ 21 | } -------------------------------------------------------------------------------- /example_ohos/ohos/hvigorw: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # ---------------------------------------------------------------------------- 17 | # Hvigor startup script, version 1.0.0 18 | # 19 | # Required ENV vars: 20 | # ------------------ 21 | # NODE_HOME - location of a Node home dir 22 | # or 23 | # Add /usr/local/nodejs/bin to the PATH environment variable 24 | # ---------------------------------------------------------------------------- 25 | 26 | HVIGOR_APP_HOME=$(dirname $(readlink -f $0)) 27 | HVIGOR_WRAPPER_SCRIPT=${HVIGOR_APP_HOME}/hvigor/hvigor-wrapper.js 28 | warn() { 29 | echo "" 30 | echo -e "\033[1;33m`date '+[%Y-%m-%d %H:%M:%S]'`$@\033[0m" 31 | } 32 | 33 | error() { 34 | echo "" 35 | echo -e "\033[1;31m`date '+[%Y-%m-%d %H:%M:%S]'`$@\033[0m" 36 | } 37 | 38 | fail() { 39 | error "$@" 40 | exit 1 41 | } 42 | 43 | # Determine node to start hvigor wrapper script 44 | if [ -n "${NODE_HOME}" ];then 45 | EXECUTABLE_NODE="${NODE_HOME}/bin/node" 46 | if [ ! -x "$EXECUTABLE_NODE" ];then 47 | fail "ERROR: NODE_HOME is set to an invalid directory,check $NODE_HOME\n\nPlease set NODE_HOME in your environment to the location where your nodejs installed" 48 | fi 49 | else 50 | EXECUTABLE_NODE="node" 51 | which ${EXECUTABLE_NODE} > /dev/null 2>&1 || fail "ERROR: NODE_HOME is not set and not 'node' command found in your path" 52 | fi 53 | 54 | # Check hvigor wrapper script 55 | if [ ! -r "$HVIGOR_WRAPPER_SCRIPT" ];then 56 | fail "ERROR: Couldn't find hvigor/hvigor-wrapper.js in ${HVIGOR_APP_HOME}" 57 | fi 58 | 59 | # start hvigor-wrapper script 60 | exec "${EXECUTABLE_NODE}" \ 61 | "${HVIGOR_WRAPPER_SCRIPT}" "$@" 62 | -------------------------------------------------------------------------------- /example_ohos/ohos/hvigorw.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Hvigor startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 17 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 18 | 19 | set WRAPPER_MODULE_PATH=%APP_HOME%\hvigor\hvigor-wrapper.js 20 | set NODE_EXE=node.exe 21 | 22 | goto start 23 | 24 | :start 25 | @rem Find node.exe 26 | if defined NODE_HOME goto findNodeFromNodeHome 27 | 28 | %NODE_EXE% --version >NUL 2>&1 29 | if "%ERRORLEVEL%" == "0" goto execute 30 | 31 | echo. 32 | echo ERROR: NODE_HOME is not set and no 'node' command could be found in your PATH. 33 | echo. 34 | echo Please set the NODE_HOME variable in your environment to match the 35 | echo location of your NodeJs installation. 36 | 37 | goto fail 38 | 39 | :findNodeFromNodeHome 40 | set NODE_HOME=%NODE_HOME:"=% 41 | set NODE_EXE_PATH=%NODE_HOME%/%NODE_EXE% 42 | 43 | if exist "%NODE_EXE_PATH%" goto execute 44 | echo. 45 | echo ERROR: NODE_HOME is not set and no 'node' command could be found in your PATH. 46 | echo. 47 | echo Please set the NODE_HOME variable in your environment to match the 48 | echo location of your NodeJs installation. 49 | 50 | goto fail 51 | 52 | :execute 53 | @rem Execute hvigor 54 | "%NODE_EXE%" %WRAPPER_MODULE_PATH% %* 55 | 56 | if "%ERRORLEVEL%" == "0" goto hvigorwEnd 57 | 58 | :fail 59 | exit /b 1 60 | 61 | :hvigorwEnd 62 | if "%OS%" == "Windows_NT" endlocal 63 | 64 | :end 65 | -------------------------------------------------------------------------------- /example_ohos/ohos/oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example_ohos", 3 | "version": "1.0.0", 4 | "description": "Please describe the basic information.", 5 | "main": "", 6 | "author": "", 7 | "license": "", 8 | "dependencies": { 9 | "@ohos/flutter_ohos": "file:./har/flutter.har" 10 | }, 11 | "devDependencies": { 12 | "@ohos/hypium": "1.0.6" 13 | }, 14 | "overrides": { 15 | "@ohos/flutter_ohos": "file:./har/flutter.har" 16 | } 17 | } -------------------------------------------------------------------------------- /example_ohos/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: photo_manager_example_ohos 2 | description: Demonstrates how to use the photo_manager plugin. 3 | publish_to: none 4 | 5 | environment: 6 | sdk: ">=2.19.0" 7 | flutter: ">=3.7.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | photo_manager: # override 14 | photo_manager_example: 15 | path: ../example 16 | 17 | photo_manager_image_provider: # override 18 | video_player: # override 19 | url_launcher: # override 20 | path_provider: # override 21 | 22 | dev_dependencies: 23 | flutter_lints: any 24 | flutter_test: 25 | sdk: flutter 26 | test: any 27 | 28 | flutter: 29 | uses-material-design: true 30 | 31 | dependency_overrides: 32 | photo_manager: 33 | path: ../ 34 | photo_manager_image_provider: ^1.1.1 35 | 36 | path_provider: 37 | git: 38 | url: https://gitee.com/openharmony-sig/flutter_packages 39 | path: packages/path_provider/path_provider 40 | url_launcher: 41 | git: 42 | url: https://gitee.com/openharmony-sig/flutter_packages 43 | path: packages/url_launcher/url_launcher 44 | video_player: 45 | git: 46 | url: https://gitee.com/openharmony-sig/flutter_packages 47 | path: packages/video_player/video_player 48 | -------------------------------------------------------------------------------- /flow_chart/advance_custom_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_photo_manager/e59d65ad598eb9d0f4925afd595e4226fc1006ae/flow_chart/advance_custom_filter.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 26 | # Carthage/Checkouts 27 | 28 | Carthage/Build 29 | 30 | # We recommend against adding the Pods directory to your .gitignore. However 31 | # you should judge for yourself, the pros and cons are mentioned at: 32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 33 | # 34 | # Note: if you ignore the Pods directory, make sure to uncomment 35 | # `pod install` in .travis.yml 36 | # 37 | # Pods/ 38 | -------------------------------------------------------------------------------- /ios/Classes/PMConverter.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "PMConvertProtocol.h" 3 | 4 | @interface PMConverter : NSObject 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Classes/PMConverter.m: -------------------------------------------------------------------------------- 1 | #import "PMConverter.h" 2 | #import "PMImport.h" 3 | 4 | @implementation PMConverter { 5 | 6 | } 7 | 8 | - (id)convertData:(NSData *)data { 9 | return [FlutterStandardTypedData typedDataWithBytes:data]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /ios/Classes/PMImport.h: -------------------------------------------------------------------------------- 1 | // 2 | // PMImport.h 3 | // Pods 4 | // 5 | 6 | #ifndef PMImport_h 7 | #define PMImport_h 8 | 9 | #if TARGET_OS_OSX 10 | #import 11 | #elif TARGET_OS_IOS 12 | #import 13 | #endif 14 | 15 | #endif /* PMImport_h */ 16 | -------------------------------------------------------------------------------- /ios/Classes/PMNotificationManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "PMImport.h" 3 | 4 | @protocol FlutterPluginRegistrar; 5 | 6 | @interface PMNotificationManager : NSObject 7 | @property(nonatomic, strong) NSObject *registrar; 8 | 9 | - (instancetype)initWithRegistrar:(NSObject *)registrar; 10 | 11 | - (void)startNotify; 12 | 13 | - (void)stopNotify; 14 | 15 | + (instancetype)managerWithRegistrar:(NSObject *)registrar; 16 | 17 | - (BOOL)isNotifying; 18 | 19 | - (void)detach; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /ios/Classes/PMPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "PMImport.h" 3 | #import 4 | 5 | @class PMManager; 6 | @class PMNotificationManager; 7 | 8 | @interface PMPlugin : NSObject 9 | @property(nonatomic, strong) PMManager *manager; 10 | @property(nonatomic, strong) PMNotificationManager *notificationManager; 11 | - (void)registerPlugin:(NSObject *)registrar; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Classes/PMProgressHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // PMProgressHandler.h 3 | // path_provider 4 | // 5 | 6 | #import 7 | #import "PMProgressHandlerProtocol.h" 8 | #import "PMImport.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface PMProgressHandler : NSObject 13 | 14 | @property(nonatomic, assign) int channelIndex; 15 | 16 | - (void)notify:(double)progress state:(PMProgressState)state; 17 | 18 | - (void)register:(NSObject *)registrar channelIndex:(int)index; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /ios/Classes/PMProgressHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // PMProgressHandler.m 3 | // path_provider 4 | // 5 | 6 | #import "PMProgressHandler.h" 7 | 8 | @implementation PMProgressHandler { 9 | FlutterMethodChannel *channel; 10 | } 11 | 12 | - (void)notify:(double)progress state:(PMProgressState)state { 13 | int s = state; 14 | NSDictionary *dict = @{ 15 | @"state": @(s), 16 | @"progress": @(progress), 17 | }; 18 | 19 | if (!channel) { 20 | return; 21 | } 22 | 23 | // Use the main thread to invoke the method. 24 | dispatch_async(dispatch_get_main_queue(), ^{ 25 | @try { 26 | FlutterMethodChannel *channel = self->channel; 27 | if (!channel) { 28 | return; 29 | } 30 | // Try to invoke the channel in the thread, regardless if it's available. 31 | [channel invokeMethod:@"notifyProgress" arguments:dict]; 32 | } @catch (NSException *exception) { 33 | // Do nothing when it throws. 34 | } @finally { 35 | if (state == PMProgressStateSuccess || state == PMProgressStateFailed) { 36 | [self->channel setMethodCallHandler:nil]; 37 | self->channel = nil; 38 | } 39 | } 40 | }); 41 | } 42 | 43 | - (void)register:(NSObject *)registrar channelIndex:(int)index { 44 | NSString *name = [NSString stringWithFormat:@"com.fluttercandies/photo_manager/progress/%d", index]; 45 | channel = [FlutterMethodChannel methodChannelWithName:name binaryMessenger:registrar.messenger]; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /ios/Classes/PhotoManagerPlugin.h: -------------------------------------------------------------------------------- 1 | #import "PMImport.h" 2 | 3 | @interface PhotoManagerPlugin : NSObject 4 | @property(nonatomic, strong) NSObject *registrar; 5 | @end 6 | -------------------------------------------------------------------------------- /ios/Classes/PhotoManagerPlugin.m: -------------------------------------------------------------------------------- 1 | #import "PhotoManagerPlugin.h" 2 | #import "PMPlugin.h" 3 | 4 | @implementation PhotoManagerPlugin { 5 | } 6 | + (void)registerWithRegistrar:(NSObject *)registrar { 7 | PMPlugin *plugin = [PMPlugin new]; 8 | [plugin registerPlugin:registrar]; 9 | [registrar addApplicationDelegate:plugin]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /ios/Classes/ResultHandler.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "PMImport.h" 3 | #import "PMResultHandler.h" 4 | 5 | @interface ResultHandler : NSObject 6 | 7 | @property (nonatomic, strong) FlutterMethodCall* call; 8 | @property(nonatomic, strong) FlutterResult result; 9 | 10 | - (instancetype)initWithResult:(FlutterResult)result; 11 | 12 | - (instancetype)initWithCall:(FlutterMethodCall *)call result:(FlutterResult)result; 13 | 14 | + (instancetype)handlerWithCall:(FlutterMethodCall *)call result:(FlutterResult)result; 15 | 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /ios/Classes/core/AssetEntity.h: -------------------------------------------------------------------------------- 1 | // 2 | // AssetEntity.h 3 | // photo_manager 4 | // 5 | 6 | #import 7 | #import 8 | 9 | @interface AssetEntity : NSObject 10 | 11 | @property(nonatomic,strong) PHAsset *asset; 12 | @property(nonatomic, assign) BOOL isCloud; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /ios/Classes/core/AssetEntity.m: -------------------------------------------------------------------------------- 1 | // 2 | // AssetEntity.m 3 | // photo_manager 4 | // 5 | 6 | #import "AssetEntity.h" 7 | 8 | @implementation AssetEntity 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /ios/Classes/core/NSString+PM_COMMON.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSString (PM_COMMON) 4 | 5 | - (BOOL)isEmpty; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /ios/Classes/core/NSString+PM_COMMON.m: -------------------------------------------------------------------------------- 1 | #import "NSString+PM_COMMON.h" 2 | 3 | @implementation NSString (PM_COMMON) 4 | 5 | - (BOOL)isEmpty { 6 | if (self.length == 0) { 7 | return YES; 8 | } 9 | if ([self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]].length == 0) { 10 | return YES; 11 | } 12 | return NO; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/core/PHAsset+PM_COMMON.h: -------------------------------------------------------------------------------- 1 | // 2 | // PHAsset+PHAsset_checkType.h 3 | // photo_manager 4 | // 5 | 6 | #import 7 | #import "PMLogUtils.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | @interface PHAsset (PM_COMMON) 12 | 13 | - (bool)isImage; 14 | - (bool)isVideo; 15 | - (bool)isAudio; 16 | - (bool)isImageOrVideo; 17 | - (bool)isLivePhoto; 18 | 19 | - (NSString*)title; 20 | 21 | - (NSString *)filenameWithOptions:(int)subtype isOrigin:(BOOL)isOrigin fileType:(AVFileType)fileType; 22 | 23 | /** 24 | Get the MIME type for this asset from UTI (`PHAssetResource.uniformTypeIdentifier`), such as `image/jpeg`, `image/heic`, `video/quicktime`, etc. 25 | 26 | @note For Live Photos, this returns a type representing its image file. 27 | @return The MIME type of this asset if available, otherwise `nil`. 28 | */ 29 | - (nullable NSString*)mimeType; 30 | - (PHAssetResource *)getCurrentResource; 31 | - (void)requestCurrentResourceData:(void (^)(NSData *_Nullable result))block; 32 | - (PHAssetResource *)getLivePhotosResource; 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /ios/Classes/core/PHAssetCollection+PM_COMMON.h: -------------------------------------------------------------------------------- 1 | // 2 | // PHAssetCollection+PHAssetCollection_obtainAssetCount.h 3 | // photo_manager 4 | // 5 | // Created by Alex on 2022/2/24. 6 | // 7 | 8 | #import 9 | 10 | @interface PHAssetCollection (PM_COMMON) 11 | 12 | - (NSUInteger)obtainAssetCount:(PHFetchOptions *)options; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /ios/Classes/core/PHAssetCollection+PM_COMMON.m: -------------------------------------------------------------------------------- 1 | // 2 | // PHAssetCollection+PHAssetCollection_obtainAssetCount.m 3 | // photo_manager 4 | // 5 | // Created by Alex on 2022/2/24. 6 | // 7 | 8 | #import "PHAssetCollection+PM_COMMON.h" 9 | 10 | @implementation PHAssetCollection (PM_COMMON) 11 | 12 | - (NSUInteger)obtainAssetCount:(PHFetchOptions *)options { 13 | PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:self options:options]; 14 | NSUInteger count = fetchResult.count; 15 | return count; 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /ios/Classes/core/PHAssetResource+PM_COMMON.h: -------------------------------------------------------------------------------- 1 | // 2 | // PHAssetResource+PHAssetResource_checkType.h 3 | // photo_manager 4 | // 5 | 6 | #import 7 | 8 | @interface PHAssetResource (PM_COMMON) 9 | 10 | - (bool)isImage; 11 | - (bool)isVideo; 12 | - (bool)isAudio; 13 | - (bool)isImageOrVideo; 14 | - (bool)isValid; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/Classes/core/PHAssetResource+PM_COMMON.m: -------------------------------------------------------------------------------- 1 | // 2 | // PHAssetResource+PHAssetResource_checkType.m 3 | // photo_manager 4 | // 5 | 6 | #import "PHAssetResource+PM_COMMON.h" 7 | 8 | @implementation PHAssetResource (PM_COMMON) 9 | 10 | - (bool)isImage { 11 | return [self type] == PHAssetResourceTypePhoto 12 | || [self type] == PHAssetResourceTypeAlternatePhoto 13 | || [self type] == PHAssetResourceTypeFullSizePhoto 14 | || [self type] == PHAssetResourceTypeAdjustmentBasePhoto; 15 | } 16 | 17 | - (bool)isVideo { 18 | BOOL predicate = [self type] == PHAssetResourceTypeVideo || PHAssetResourceTypeFullSizeVideo; 19 | if (@available(iOS 9.1, *)) { 20 | predicate = (predicate || [self type] == PHAssetResourceTypePairedVideo); 21 | } 22 | if (@available(iOS 10.0, *)) { 23 | predicate = (predicate || [self type] == PHAssetResourceTypeFullSizePairedVideo); 24 | predicate = (predicate || [self type] == PHAssetResourceTypeAdjustmentBasePairedVideo); 25 | } 26 | if (@available(iOS 13.0, *)) { 27 | predicate = (predicate || [self type] == PHAssetResourceTypeAdjustmentBaseVideo); 28 | } 29 | return predicate; 30 | } 31 | 32 | - (bool)isAudio { 33 | return [self type] == PHAssetResourceTypeAudio; 34 | } 35 | 36 | - (bool)isImageOrVideo { 37 | return [self isVideo] || [self isImage]; 38 | } 39 | 40 | - (bool)isValid { 41 | bool isResource = self.type != PHAssetResourceTypeAdjustmentData; 42 | 43 | #if __IPHONE_17_0 44 | if (@available(iOS 17.0, *)) { 45 | isResource = isResource && self.type != PHAssetResourceTypePhotoProxy; 46 | } 47 | #endif 48 | return isResource; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /ios/Classes/core/PMAssetPathEntity.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #define PM_TYPE_ALBUM 1 4 | #define PM_TYPE_FOLDER 2 5 | @class PHAsset; 6 | @class PHAssetCollection; 7 | 8 | @interface PMAssetPathEntity : NSObject 9 | 10 | @property(nonatomic, copy) NSString *id; 11 | @property(nonatomic, copy) NSString *name; 12 | @property(nonatomic, assign) BOOL isAll; 13 | @property(nonatomic, assign) int type; 14 | @property(nonatomic, assign) NSUInteger assetCount; 15 | @property(nonatomic, assign) long modifiedDate; 16 | @property(nonatomic, strong) PHAssetCollection *collection; 17 | 18 | + (instancetype)entityWithId:(NSString *)id name:(NSString *)name assetCollection:(PHAssetCollection*)collection; 19 | 20 | @end 21 | 22 | @interface PMAssetEntity : NSObject 23 | 24 | @property(nonatomic, copy) NSString *id; 25 | @property(nonatomic, assign) long createDt; 26 | @property(nonatomic, assign) NSUInteger width; 27 | @property(nonatomic, assign) NSUInteger height; 28 | @property(nonatomic, assign) long duration; 29 | @property(nonatomic, assign) int type; 30 | @property(nonatomic, strong) PHAsset *phAsset; 31 | @property(nonatomic, assign) long modifiedDt; 32 | @property(nonatomic, assign) double lat; 33 | @property(nonatomic, assign) double lng; 34 | @property(nonatomic, copy) NSString *title; 35 | @property(nonatomic, assign) NSUInteger subtype; 36 | @property(nonatomic, assign) BOOL favorite; 37 | @property(nonatomic, assign) BOOL isLocallyAvailable; 38 | 39 | - (instancetype)initWithId:(NSString *)id 40 | createDt:(long)createDt 41 | width:(NSUInteger)width 42 | height:(NSUInteger)height 43 | duration:(long)duration 44 | type:(int)type; 45 | 46 | + (instancetype)entityWithId:(NSString *)id 47 | createDt:(long)createDt 48 | width:(NSUInteger)width 49 | height:(NSUInteger)height 50 | duration:(long)duration 51 | type:(int)type; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /ios/Classes/core/PMAssetPathEntity.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "PMAssetPathEntity.h" 3 | 4 | @implementation PMAssetPathEntity {} 5 | 6 | - (instancetype)init { 7 | self = [super init]; 8 | if (self) { 9 | self.type = PM_TYPE_ALBUM; 10 | self.assetCount = NSIntegerMax; 11 | } 12 | return self; 13 | } 14 | 15 | - (instancetype)initWithId:(NSString *)id name:(NSString *)name assetCollection:(PHAssetCollection*)collection { 16 | self = [super init]; 17 | if (self) { 18 | self.id = id; 19 | self.name = name; 20 | self.type = PM_TYPE_ALBUM; 21 | self.assetCount = NSIntegerMax; 22 | self.collection = collection; 23 | } 24 | return self; 25 | } 26 | 27 | + (instancetype)entityWithId:(NSString *)id name:(NSString *)name assetCollection:(PHAssetCollection *)collection { 28 | return [[self alloc] initWithId:id name:name assetCollection:collection]; 29 | } 30 | 31 | @end 32 | 33 | @implementation PMAssetEntity {} 34 | 35 | - (instancetype)initWithId:(NSString *)id createDt:(long)createDt width:(NSUInteger)width height:(NSUInteger)height 36 | duration:(long)duration type:(int)type { 37 | self = [super init]; 38 | if (self) { 39 | self.id = id; 40 | self.createDt = createDt; 41 | self.width = width; 42 | self.height = height; 43 | self.duration = duration; 44 | self.type = type; 45 | } 46 | return self; 47 | } 48 | 49 | + (instancetype)entityWithId:(NSString *)id createDt:(long)createDt width:(NSUInteger)width height:(NSUInteger)height 50 | duration:(long)duration type:(int)type { 51 | return [[self alloc] initWithId:id createDt:createDt width:width height:height duration:duration type:type]; 52 | } 53 | 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /ios/Classes/core/PMBaseFilter.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jinglong cai on 2023/2/9. 3 | // 4 | 5 | #import 6 | 7 | @protocol PMBaseFilter 8 | 9 | - (PHFetchOptions *)getFetchOptions:(int)type; 10 | 11 | - (BOOL) containsModified; 12 | 13 | - (BOOL) needTitle; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/core/PMCacheContainer.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class PMAssetEntity; 4 | @class AVPlayerItem; 5 | 6 | @interface PMCacheContainer : NSObject 7 | 8 | - (void)putAssetEntity:(PMAssetEntity *)entity; 9 | 10 | - (PMAssetEntity *)getAssetEntity:(NSString *)id; 11 | 12 | - (void)clearCache; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /ios/Classes/core/PMCacheContainer.m: -------------------------------------------------------------------------------- 1 | #import "PMCacheContainer.h" 2 | #import "PMAssetPathEntity.h" 3 | #import 4 | 5 | @implementation PMCacheContainer { 6 | NSMutableDictionary *map; 7 | } 8 | 9 | - (instancetype)init { 10 | self = [super init]; 11 | if (self) { 12 | map = [NSMutableDictionary new]; 13 | } 14 | 15 | return self; 16 | } 17 | 18 | - (void)putAssetEntity:(PMAssetEntity *)entity { 19 | @synchronized (map) { 20 | self->map[entity.id] = entity; 21 | } 22 | } 23 | 24 | - (PMAssetEntity *)getAssetEntity:(NSString *)id { 25 | return map[id]; 26 | } 27 | 28 | - (void)clearCache { 29 | @synchronized (map) { 30 | [self->map removeAllObjects]; 31 | } 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /ios/Classes/core/PMConvertProtocol.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @protocol PMConvertProtocol 4 | 5 | - (id)convertData:(NSData *)data; 6 | 7 | @end -------------------------------------------------------------------------------- /ios/Classes/core/PMConvertUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "PMBaseFilter.h" 4 | 5 | @class PMAssetPathEntity; 6 | @class PMAssetEntity; 7 | @class PMFilterOption; 8 | @class PMFilterOptionGroup; 9 | 10 | @interface PMConvertUtils : NSObject 11 | 12 | + (NSDictionary *)convertPathToMap:(NSArray *)array; 13 | 14 | + (NSDictionary *)convertAssetToMap:(NSArray *)array 15 | optionGroup:(NSObject *)optionGroup; 16 | 17 | + (NSDictionary *)convertPHAssetToMap:(PHAsset *)asset 18 | needTitle:(BOOL)needTitle; 19 | 20 | + (NSDictionary *)convertPMAssetToMap:(PMAssetEntity *)asset 21 | needTitle:(BOOL)needTitle; 22 | 23 | + (PMFilterOption *)convertMapToPMFilterOption:(NSDictionary *)map; 24 | 25 | + (NSObject *)convertMapToOptionContainer:(NSDictionary *)map; 26 | 27 | + (AVFileType)convertNumberToAVFileType:(int)number; 28 | 29 | + (NSString *)convertAVFileTypeToExtension:(AVFileType)fileType; 30 | @end 31 | -------------------------------------------------------------------------------- /ios/Classes/core/PMFileHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // PMFileHelper.h 3 | // photo_manager 4 | // 5 | 6 | #import 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /// Contains access file methods 11 | @interface PMFileHelper : NSObject 12 | 13 | +(void)deleteFile:(NSString *)path isDirectory:(BOOL)isDirectory error:(NSError *)error; 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /ios/Classes/core/PMFileHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // PMFileHelper.m 3 | // photo_manager 4 | // 5 | 6 | #import "PMFileHelper.h" 7 | 8 | @implementation PMFileHelper 9 | 10 | + (void)deleteFile:(NSString *)path isDirectory:(BOOL)isDirectory error:(NSError *)error { 11 | NSFileManager *fileManager = NSFileManager.defaultManager; 12 | BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory]; 13 | if (exists) { 14 | [fileManager removeItemAtPath:path error:&error]; 15 | } 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /ios/Classes/core/PMFilterOption.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "PMBaseFilter.h" 3 | 4 | @interface PMDateOption : NSObject 5 | 6 | @property(nonatomic, strong) NSDate *min; 7 | @property(nonatomic, strong) NSDate *max; 8 | @property(nonatomic, assign) BOOL ignore; 9 | 10 | - (NSString *)dateCond:(NSString *)key; 11 | 12 | - (NSArray *)dateArgs; 13 | 14 | @end 15 | 16 | typedef struct PMSizeConstraint { 17 | 18 | unsigned int minWidth; 19 | unsigned int maxWidth; 20 | unsigned int minHeight; 21 | unsigned int maxHeight; 22 | BOOL ignoreSize; 23 | 24 | } PMSizeConstraint; 25 | 26 | typedef struct PMDurationConstraint { 27 | 28 | double minDuration; 29 | double maxDuration; 30 | BOOL allowNullable; 31 | 32 | } PMDurationConstraint; 33 | 34 | @interface PMFilterOption : NSObject 35 | 36 | @property(nonatomic, assign) BOOL needTitle; 37 | @property(nonatomic, assign) PMSizeConstraint sizeConstraint; 38 | @property(nonatomic, assign) PMDurationConstraint durationConstraint; 39 | 40 | - (NSString *)sizeCond; 41 | 42 | - (NSArray *)sizeArgs; 43 | 44 | - (NSString *)durationCond; 45 | 46 | - (NSArray *)durationArgs; 47 | 48 | @end 49 | 50 | 51 | @interface PMFilterOptionGroup : NSObject 52 | 53 | @property(nonatomic, strong) PMFilterOption *imageOption; 54 | @property(nonatomic, strong) PMFilterOption *videoOption; 55 | @property(nonatomic, strong) PMFilterOption *audioOption; 56 | @property(nonatomic, strong) PMDateOption *dateOption; 57 | @property(nonatomic, strong) PMDateOption *updateOption; 58 | @property(nonatomic, assign) BOOL containsLivePhotos; 59 | @property(nonatomic, assign) BOOL onlyLivePhotos; 60 | @property(nonatomic, assign) BOOL containsModified; 61 | @property(nonatomic, assign) BOOL includeHiddenAssets; 62 | @property(nonatomic, strong) NSArray *sortArray; 63 | 64 | - (NSArray *)sortCond; 65 | 66 | - (void)injectSortArray:(NSArray *)array; 67 | @end 68 | 69 | 70 | @interface PMCustomFilterOption : NSObject 71 | @property (nonatomic, strong) NSDictionary *params; 72 | @end 73 | -------------------------------------------------------------------------------- /ios/Classes/core/PMFolderUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface PMFolderUtils : NSObject 5 | 6 | + (NSArray *)getRootFolderWithOptions:(PHFetchOptions *)options; 7 | 8 | + (BOOL)isRecentCollection:(NSString *)id1; 9 | 10 | + (NSArray *)getSubCollectionWithCollection:(PHCollectionList *)collection 11 | options:(PHFetchOptions *)options; 12 | 13 | +(void)debugInfo:(PHCollection*)collection ; 14 | 15 | @end -------------------------------------------------------------------------------- /ios/Classes/core/PMFolderUtils.m: -------------------------------------------------------------------------------- 1 | #import "PMFolderUtils.h" 2 | #import "PMLogUtils.h" 3 | 4 | @implementation PMFolderUtils { 5 | 6 | } 7 | 8 | + (NSArray *)getRootFolderWithOptions:(PHFetchOptions *)options { 9 | PHFetchResult *result = [PHCollection fetchTopLevelUserCollectionsWithOptions:options]; 10 | 11 | NSMutableArray *array = [NSMutableArray new]; 12 | 13 | for (PHCollection *item in result) { 14 | if ([item isMemberOfClass:PHCollectionList.class]) { 15 | [array addObject:item]; 16 | } 17 | } 18 | 19 | return array; 20 | } 21 | 22 | + (BOOL)isRecentCollection:(NSString *)id { 23 | PHFetchResult *result = [PHAssetCollection 24 | fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum 25 | subtype:PHAssetCollectionSubtypeAlbumRegular 26 | options:nil]; 27 | 28 | if (result && result.count) { 29 | for (PHAssetCollection *collection in result) { 30 | if (collection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumUserLibrary) { 31 | return [id isEqualToString:collection.localIdentifier]; 32 | } 33 | } 34 | } 35 | 36 | return NO; 37 | } 38 | 39 | + (NSArray *)getSubCollectionWithCollection:(PHCollectionList *)collection 40 | options:(PHFetchOptions *)options { 41 | 42 | PHFetchResult *result = [PHCollection fetchCollectionsInCollectionList:collection options:nil]; 43 | 44 | NSMutableArray *array = [NSMutableArray new]; 45 | 46 | for (PHCollection *item in result) { 47 | if ([item isMemberOfClass:PHCollectionList.class] || [item isMemberOfClass:PHAssetCollection.class]) { 48 | [array addObject:item]; 49 | } 50 | } 51 | 52 | return array; 53 | } 54 | 55 | + (void)debugInfo:(PHCollection *)collection { 56 | NSString *title = collection.localizedTitle; 57 | if ([collection isMemberOfClass:PHCollectionList.class]) { 58 | [PMLogUtils.sharedInstance info: [NSString stringWithFormat:@"title = %@, type: %@", title, @"文件夹"]]; 59 | } else { 60 | [PMLogUtils.sharedInstance info: [NSString stringWithFormat:@"title = %@, type: %@", title, @"相簿"]]; 61 | } 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /ios/Classes/core/PMImageUtil.h: -------------------------------------------------------------------------------- 1 | // 2 | // PMImageUtil.h 3 | // path_provider_macos 4 | // 5 | 6 | #import 7 | #import "PMThumbLoadOption.h" 8 | 9 | #if TARGET_OS_IOS 10 | typedef UIImage PMImage; 11 | #endif 12 | 13 | #if TARGET_OS_OSX 14 | typedef NSImage PMImage; 15 | #endif 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | @interface PMImageUtil : NSObject 20 | 21 | + (NSData *)convertToData:(PMImage *)image formatType:(PMThumbFormatType)type quality:(float)quality; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /ios/Classes/core/PMImageUtil.m: -------------------------------------------------------------------------------- 1 | // 2 | // PMImageUtil.m 3 | // path_provider_macos 4 | // 5 | 6 | #import "PMImageUtil.h" 7 | 8 | @implementation PMImageUtil 9 | 10 | + (NSData *)convertToData:(PMImage *)image formatType:(PMThumbFormatType)type quality:(float)quality { 11 | 12 | #if TARGET_OS_OSX 13 | 14 | NSData *imageData = [image TIFFRepresentation]; 15 | NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData]; 16 | NSData *resultData; 17 | if (type == PMThumbFormatTypePNG) { 18 | resultData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; 19 | } else { 20 | resultData = [imageRep representationUsingType:NSBitmapImageFileTypeJPEG properties:@{ 21 | NSImageCompressionFactor: @(quality) 22 | }]; 23 | } 24 | 25 | return resultData; 26 | 27 | #endif 28 | 29 | #if TARGET_OS_IOS 30 | NSData *resultData; 31 | if (type == PMThumbFormatTypePNG) { 32 | resultData = UIImagePNGRepresentation(image); 33 | } else { 34 | resultData = UIImageJPEGRepresentation(image, quality); 35 | } 36 | 37 | return resultData; 38 | 39 | #endif 40 | } 41 | 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /ios/Classes/core/PMLogUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface PMLogUtils : NSObject 4 | 5 | @property(nonatomic, assign) BOOL isLog; 6 | 7 | + (instancetype)sharedInstance; 8 | 9 | - (void)info:(NSString *)info; 10 | - (void)debug:(NSString *)info; 11 | 12 | @end -------------------------------------------------------------------------------- /ios/Classes/core/PMLogUtils.m: -------------------------------------------------------------------------------- 1 | #import "PMLogUtils.h" 2 | 3 | @implementation PMLogUtils { 4 | 5 | } 6 | + (instancetype)sharedInstance { 7 | static PMLogUtils *_sharedInstance = nil; 8 | static dispatch_once_t onceToken; 9 | dispatch_once(&onceToken, ^{ 10 | _sharedInstance = [[self alloc] init]; 11 | _sharedInstance.isLog = NO; 12 | }); 13 | 14 | return _sharedInstance; 15 | } 16 | 17 | - (void)info:(NSString *)info { 18 | if (!self.isLog) { 19 | return; 20 | } 21 | NSLog(@"PhotoManager info: %@", info); 22 | } 23 | 24 | - (void)debug:(NSString *)info { 25 | if (!self.isLog) { 26 | return; 27 | } 28 | NSLog(@"PhotoManager debug: %@", info); 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /ios/Classes/core/PMMD5Utils.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #define PMFileHashDefaultChunkSizeForReadingData 1024*8 // 8K 5 | 6 | @interface PMMD5Utils : NSObject 7 | 8 | // 计算 NSData 的 MD5 9 | + (NSString *)getMD5FromData:(NSData *)data; 10 | 11 | // 计算字符串的 MD5 12 | + (NSString *)getMD5FromString:(NSString *)string; 13 | 14 | // 计算文件的 MD5 15 | + (NSString *)getMD5FromPath:(NSString *)path; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /ios/Classes/core/PMPathFilterOption.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jinglong cai on 2023/7/18. 3 | // 4 | 5 | #import 6 | #import 7 | 8 | @interface PMPathFilterOption : NSObject 9 | 10 | + (instancetype)optionWithDict:(NSDictionary *)dict; 11 | 12 | @property(nonatomic, strong) NSArray *type; 13 | @property(nonatomic, assign) NSArray *subType; 14 | 15 | @end -------------------------------------------------------------------------------- /ios/Classes/core/PMPathFilterOption.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jinglong cai on 2023/7/18. 3 | // 4 | 5 | #import "PMPathFilterOption.h" 6 | 7 | 8 | @implementation PMPathFilterOption { 9 | 10 | } 11 | + (instancetype)optionWithDict:(NSDictionary *)dict { 12 | PMPathFilterOption *option = [PMPathFilterOption new]; 13 | NSDictionary *darwinDict = dict[@"darwin"]; 14 | 15 | option.type = darwinDict[@"type"];; 16 | option.subType = darwinDict[@"subType"];; 17 | 18 | return option; 19 | } 20 | 21 | @end -------------------------------------------------------------------------------- /ios/Classes/core/PMProgressHandlerProtocol.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef enum PMProgressState{ 4 | PMProgressStatePrepare = 0, 5 | PMProgressStateLoading = 1, 6 | PMProgressStateSuccess = 2, 7 | PMProgressStateFailed = 3, 8 | } PMProgressState; 9 | 10 | 11 | @protocol PMProgressHandlerProtocol 12 | 13 | - (void)notify:(double)progress state:(PMProgressState)state; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/core/PMRequestTypeUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface PMRequestTypeUtils : NSObject 4 | 5 | + (BOOL)containsImage:(int)type; 6 | 7 | + (BOOL)containsVideo:(int)type; 8 | 9 | + (BOOL)containsAudio:(int)type; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /ios/Classes/core/PMRequestTypeUtils.m: -------------------------------------------------------------------------------- 1 | #import "PMRequestTypeUtils.h" 2 | 3 | #define PM_TYPE_IMAGE 1 4 | #define PM_TYPE_VIDEO 1<<1 5 | #define PM_TYPE_AUDIO 1<<2 6 | 7 | @implementation PMRequestTypeUtils { 8 | 9 | } 10 | 11 | + (BOOL)checkContainsType:(int)type targetType:(int)targetType { 12 | return (type & targetType) == targetType; 13 | } 14 | 15 | + (BOOL)containsImage:(int)type { 16 | return [self checkContainsType:type targetType:PM_TYPE_IMAGE]; 17 | } 18 | 19 | + (BOOL)containsVideo:(int)type { 20 | return [self checkContainsType:type targetType:PM_TYPE_VIDEO]; 21 | } 22 | 23 | + (BOOL)containsAudio:(int)type { 24 | return [self checkContainsType:type targetType:PM_TYPE_AUDIO]; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /ios/Classes/core/PMResultHandler.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @protocol PMResultHandler 4 | 5 | - (void)replyError:(NSObject *)value; 6 | 7 | - (void)reply:(id)obj; 8 | 9 | - (void)notImplemented; 10 | 11 | - (BOOL)isReplied; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Classes/core/PMThumbLoadOption.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | typedef enum PMThumbFormatType { 5 | PMThumbFormatTypeJPEG, 6 | PMThumbFormatTypePNG, 7 | } PMThumbFormatType; 8 | 9 | @interface PMThumbLoadOption : NSObject 10 | 11 | @property(nonatomic, assign) int width; 12 | @property(nonatomic, assign) int height; 13 | @property(nonatomic, assign) PMThumbFormatType format; 14 | @property(nonatomic, assign) float quality; 15 | 16 | #pragma mark only iOS 17 | @property(nonatomic, assign) PHImageContentMode contentMode; 18 | @property(nonatomic, assign) PHImageRequestOptionsDeliveryMode deliveryMode; 19 | @property(nonatomic, assign) PHImageRequestOptionsResizeMode resizeMode; 20 | 21 | + (instancetype)optionDict:(NSDictionary *)dict; 22 | 23 | -(CGSize)makeSize; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /ios/Classes/core/Reply.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface Reply : NSObject 4 | 5 | @property (nonatomic, assign) BOOL isReply; 6 | 7 | - (instancetype)initWithIsReply:(BOOL)isReply; 8 | 9 | + (instancetype)replyWithIsReply:(BOOL)isReply; 10 | 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /ios/Classes/core/Reply.m: -------------------------------------------------------------------------------- 1 | #import "Reply.h" 2 | 3 | @implementation Reply { 4 | 5 | } 6 | 7 | - (instancetype)initWithIsReply:(BOOL)isReply { 8 | self = [super init]; 9 | if (self) { 10 | self.isReply = isReply; 11 | } 12 | 13 | return self; 14 | } 15 | 16 | + (instancetype)replyWithIsReply:(BOOL)isReply { 17 | return [[self alloc] initWithIsReply:isReply]; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /ios/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | 8 | NSPrivacyCollectedDataType 9 | NSPrivacyCollectedDataTypePhotosorVideos 10 | NSPrivacyCollectedDataTypeLinked 11 | 12 | NSPrivacyCollectedDataTypeTracking 13 | 14 | NSPrivacyCollectedDataTypePurposes 15 | 16 | NSPrivacyCollectedDataTypePurposeProductPersonalization 17 | NSPrivacyCollectedDataTypePurposeOther 18 | 19 | 20 | 21 | NSPrivacyTrackingDomains 22 | 23 | NSPrivacyAccessedAPITypes 24 | 25 | 26 | NSPrivacyAccessedAPIType 27 | NSPrivacyAccessedAPICategoryFileTimestamp 28 | NSPrivacyAccessedAPITypeReasons 29 | 30 | C617.1 31 | 32 | 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/photo_manager.podspec: -------------------------------------------------------------------------------- 1 | pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) 2 | library_version = pubspec['version'].gsub('+', '-') 3 | 4 | Pod::Spec.new do |s| 5 | s.name = 'photo_manager' 6 | s.version = library_version 7 | s.summary = 'Photo management APIs for Flutter.' 8 | s.description = <<-DESC 9 | A Flutter plugin that provides assets abstraction management APIs. 10 | DESC 11 | s.homepage = 'https://github.com/fluttercandies/flutter_photo_manager' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'CaiJingLong' => 'cjl_spy@163.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h','Classes/**/**/*.h' 17 | s.osx.dependency 'FlutterMacOS' 18 | s.ios.dependency 'Flutter' 19 | 20 | s.ios.framework = 'Photos' 21 | s.ios.framework = 'PhotosUI' 22 | 23 | s.osx.framework = 'Photos' 24 | s.osx.framework = 'PhotosUI' 25 | 26 | s.ios.deployment_target = '9.0' 27 | s.osx.deployment_target = '10.15' 28 | 29 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } 30 | s.swift_version = '5.0' 31 | 32 | s.resource_bundles = {'photo_manager_privacy' => ['Resources/PrivacyInfo.xcprivacy']} 33 | end 34 | -------------------------------------------------------------------------------- /lib/photo_manager.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | /// The main library that contains all functions integrating with photo library. 6 | /// 7 | /// To use, import `package:photo_manager/photo_manager.dart`. 8 | library photo_manager; 9 | 10 | export 'src/filter/base_filter.dart'; 11 | export 'src/filter/classical/filter_option_group.dart'; 12 | export 'src/filter/classical/filter_options.dart'; 13 | export 'src/filter/custom/advance.dart'; 14 | export 'src/filter/custom/custom_columns.dart'; 15 | export 'src/filter/custom/custom_filter.dart'; 16 | export 'src/filter/custom/order_by_item.dart'; 17 | export 'src/filter/path_filter.dart'; 18 | 19 | export 'src/internal/enums.dart'; 20 | export 'src/internal/extensions.dart' show PermissionStateExt; 21 | export 'src/internal/plugin.dart' show PhotoManagerPlugin; 22 | export 'src/internal/progress_handler.dart'; 23 | 24 | export 'src/managers/caching_manager.dart'; 25 | export 'src/managers/notify_manager.dart'; 26 | export 'src/managers/photo_manager.dart'; 27 | 28 | export 'src/types/entity.dart'; 29 | export 'src/types/thumbnail.dart'; 30 | export 'src/types/types.dart'; 31 | 32 | export 'src/utils/column_utils.dart'; 33 | -------------------------------------------------------------------------------- /lib/platform_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | library photo_manager_platform_utils; 5 | 6 | import 'dart:io'; 7 | 8 | /// An utility to match platforms. It adds a general support for 9 | /// OpenHarmony OS. 10 | class PlatformUtils { 11 | const PlatformUtils._(); 12 | 13 | /// Whether the operating system is a version of 14 | /// [ohos](https://en.wikipedia.org/wiki/OpenHarmony). 15 | static final isOhos = Platform.operatingSystem == 'ohos'; 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/filter/custom/order_by_item.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | /// {@template PM.order_by_item} 6 | /// 7 | /// The order by item. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// OrderByItem(CustomColumns.base.width, true); 12 | /// ``` 13 | /// 14 | /// See also: 15 | /// - [CustomFilter] 16 | /// - [CustomColumns.base] 17 | /// - [CustomColumns.android] 18 | /// - [CustomColumns.darwin] 19 | /// - [CustomColumns.platformValues] 20 | /// 21 | /// {@endtemplate} 22 | class OrderByItem { 23 | /// {@macro PM.order_by_item} 24 | const OrderByItem(this.column, this.isAsc); 25 | 26 | /// {@macro PM.order_by_item} 27 | const OrderByItem.desc(this.column) : isAsc = false; 28 | 29 | /// {@macro PM.order_by_item} 30 | const OrderByItem.asc(this.column) : isAsc = true; 31 | 32 | /// {@macro PM.order_by_item} 33 | const OrderByItem.named({ 34 | required this.column, 35 | this.isAsc = true, 36 | }); 37 | 38 | /// The column name. 39 | final String column; 40 | 41 | /// The order type. 42 | final bool isAsc; 43 | 44 | /// Convert to the map. 45 | Map toMap() { 46 | return { 47 | 'column': column, 48 | 'isAsc': isAsc, 49 | }; 50 | } 51 | 52 | @override 53 | String toString() { 54 | return 'OrderByItem{column: $column, isAsc: $isAsc}'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/internal/extensions.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | import 'enums.dart'; 6 | 7 | /// Provides extension methods for [PermissionState] values. 8 | extension PermissionStateExt on PermissionState { 9 | /// Returns `true` if the permission has been granted; otherwise, `false`. 10 | bool get isAuth { 11 | return this == PermissionState.authorized; 12 | } 13 | 14 | /// Returns `true` if the permission grants partial or full access to assets; otherwise, `false`. 15 | bool get hasAccess { 16 | return this == PermissionState.authorized || 17 | this == PermissionState.limited; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/internal/map_interface.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | /// Contains an abstract method toMap to indicate that it can be converted into a Map object 6 | mixin IMapMixin { 7 | /// Convert current object to a map. 8 | /// 9 | /// Usually for transfer to MethodChannel. 10 | Map toMap(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/managers/caching_manager.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | import '../internal/constants.dart'; 6 | import '../internal/plugin.dart'; 7 | import '../types/entity.dart'; 8 | import '../types/thumbnail.dart'; 9 | 10 | /// The cache manager that helps to create/remove caches with specified assets. 11 | class PhotoCachingManager { 12 | factory PhotoCachingManager() => instance; 13 | 14 | PhotoCachingManager._(); 15 | 16 | static final PhotoCachingManager instance = PhotoCachingManager._(); 17 | 18 | static const ThumbnailOption _defaultOption = ThumbnailOption( 19 | size: ThumbnailSize.square(PMConstants.vDefaultThumbnailSize), 20 | ); 21 | 22 | /// Request caching for assets. 23 | /// The method does not supported on OpenHarmony. 24 | Future requestCacheAssets({ 25 | required List assets, 26 | ThumbnailOption option = _defaultOption, 27 | }) { 28 | assert(assets.isNotEmpty); 29 | return plugin.requestCacheAssetsThumbnail( 30 | assets.map((AssetEntity e) => e.id).toList(), 31 | option, 32 | ); 33 | } 34 | 35 | /// Request caching for assets' ID. 36 | /// The method does not supported on OpenHarmony. 37 | Future requestCacheAssetsWithIds({ 38 | required List assetIds, 39 | ThumbnailOption option = _defaultOption, 40 | }) { 41 | assert(assetIds.isNotEmpty); 42 | return plugin.requestCacheAssetsThumbnail(assetIds, option); 43 | } 44 | 45 | /// Cancel all cache request. 46 | /// The method does not supported on OpenHarmony. 47 | Future cancelCacheRequest() => plugin.cancelCacheRequests(); 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/utils/column_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | /// Utility functions for working with the [CustomColumns] class. 8 | class ColumnUtils { 9 | const ColumnUtils._internal(); 10 | 11 | /// An instance of the [ColumnUtils] class. 12 | static const instance = ColumnUtils._internal(); 13 | 14 | /// Converts a [DateTime] object to the format used by [MediaStore][] on Android or [NSPredicate][] on iOS/macOS. 15 | /// 16 | /// [MediaStore]: https://developer.android.com/reference/android/provider/MediaStore 17 | /// [NSPredicate]: https://developer.apple.com/documentation/foundation/nspredicate 18 | String convertDateTimeToSql(DateTime date, {bool isSeconds = true}) { 19 | final unix = date.millisecondsSinceEpoch; 20 | 21 | if (Platform.isAndroid) { 22 | return isSeconds ? (unix ~/ 1000).toString() : unix.toString(); 23 | } else if (Platform.isIOS || Platform.isMacOS) { 24 | // The NSDate epoch starts at 2001-01-01T00:00:00Z, so we subtract this from the Unix timestamp in seconds. 25 | final secondsFrom2001 = (unix / 1000) - 978307200; 26 | final dateStr = secondsFrom2001.toStringAsFixed(6); 27 | return 'CAST($dateStr, "NSDate")'; 28 | } else { 29 | throw UnsupportedError('Unsupported platform with date'); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /log/README.md: -------------------------------------------------------------------------------- 1 | 一些帮助开发者开发的日志 2 | 3 | 安卓日志来源: https://gitee.com/kikt/media_store_simple_utils 运行的项目 4 | -------------------------------------------------------------------------------- /log/android/android-28-delete.log: -------------------------------------------------------------------------------- 1 | 2020-03-22 15:48:37.069 8286-8286/top.kikt.handledeletemedia I/My-Tag: 发生变化 = content://media/ 2 | 2020-03-22 15:48:37.069 8286-8286/top.kikt.handledeletemedia I/My-Tag: uri 中不包含id, 应该是删除 -------------------------------------------------------------------------------- /log/android/android-q-delete.log: -------------------------------------------------------------------------------- 1 | 2020-03-21 15:24:04.870 17771-17771/top.kikt.handledeletemedia I/My-Tag: 发生变化 = content://media/external/images/media 2 | 2020-03-21 15:24:04.871 17771-17771/top.kikt.handledeletemedia I/My-Tag: uri 为总uri content://media/external/images/media, 应该是删除 -------------------------------------------------------------------------------- /log/android/androidq-info.log: -------------------------------------------------------------------------------- 1 | 2020-03-22 16:01:17.043 11581-11581/top.kikt.handledeletemedia I/My-Tag: instance_id : null 2 | duration : 784 3 | orientation : null 4 | format : 12297 5 | height : null 6 | is_drm : 0 7 | bucket_display_name : Music 8 | owner_package_name : null 9 | parent : 902112 10 | volume_name : external_primary 11 | date_modified : 1579009250 12 | date_expires : null 13 | _display_name : 50c01294df9d48902639978235bcb56c6ecacc2342c54b61a91bacabae37c58d.mp3 14 | datetaken : null 15 | mime_type : audio/mpeg 16 | _id : 902113 17 | _data : /storage/emulated/0/Music/50c01294df9d48902639978235bcb56c6ecacc2342c54b61a91bacabae37c58d.mp3 18 | _hash : null 19 | _size : 3421 20 | title : 50c01294df9d48902639978235bcb56c6ecacc2342c54b61a91bacabae37c58d 21 | width : null 22 | is_trashed : 0 23 | group_id : -300030646 24 | document_id : null 25 | is_download : 0 26 | is_pending : 0 27 | date_added : 1579009250 28 | primary_directory : Music 29 | secondary_directory : null 30 | original_document_id : null 31 | bucket_id : 82896267 32 | media_type : 2 33 | relative_path : Music/ -------------------------------------------------------------------------------- /log/android/sql-28.log: -------------------------------------------------------------------------------- 1 | bucket_id IS NOT NULL AND ( ( media_type = ? AND duration >=? AND duration <=? ) ) AND datetaken <= ? ) GROUP BY (bucket_id -------------------------------------------------------------------------------- /log/iOS/ios-create.log: -------------------------------------------------------------------------------- 1 | 2020-03-21 15:37:38.493108+0800 Runner[90808:13081766] flutter: Observatory listening on http://127.0.0.1:55083/75DfBE07g2M=/ 2 | 2020-03-21 15:37:41.312862+0800 Runner[90808:13081731] [core] "Error returned from daemon: Error Domain=com.apple.accounts Code=7 "(null)"" 3 | 2020-03-21 15:37:42.999059+0800 Runner[90808:13081729] create asset : id = AA3ED26E-539B-4CE4-922D-B6AB8045A600/L0/001 4 | 2020-03-21 15:37:43.017767+0800 Runner[90808:13081731] changed = ( 5 | ) 6 | 2020-03-21 15:37:43.018169+0800 Runner[90808:13081731] inserted = ( 7 | " AA3ED26E-539B-4CE4-922D-B6AB8045A600/L0/001 mediaType=1/0, sourceType=1, (440x1325), creationDate=2020-03-21 07:37:42 +0000, location=0, hidden=0, favorite=0, adjusted=0 " 8 | ) 9 | 2020-03-21 15:37:43.018342+0800 Runner[90808:13081731] removed = ( 10 | ) 11 | 2020-03-21 15:37:43.018990+0800 Runner[90808:13081752] flutter: AssetEntity{id:AA3ED26E-539B-4CE4-922D-B6AB8045A600/L0/001} 12 | 2020-03-21 15:37:43.019402+0800 Runner[90808:13081731] on change result = { 13 | create = ( 14 | { 15 | id = "AA3ED26E-539B-4CE4-922D-B6AB8045A600/L0/001"; 16 | } 17 | ); 18 | delete = ( 19 | ); 20 | update = ( 21 | ); 22 | } 23 | 2020-03-21 15:37:43.025146+0800 Runner[90808:13081752] flutter: {delete: [], create: [{id: AA3ED26E-539B-4CE4-922D-B6AB8045A600/L0/001}], update: []} 24 | 2020-03-21 15:37:43.026938+0800 Runner[90808:13081752] flutter: {delete: [], create: [{id: AA3ED26E-539B-4CE4-922D-B6AB8045A600/L0/001}], update: []} 25 | 2020-03-21 15:37:43.340886+0800 Runner[90808:13081728] changed = (null) 26 | 2020-03-21 15:37:43.341043+0800 Runner[90808:13081728] inserted = (null) 27 | 2020-03-21 15:37:43.341174+0800 Runner[90808:13081728] removed = (null) 28 | 2020-03-21 15:37:43.341329+0800 Runner[90808:13081728] on change result = { 29 | create = ( 30 | ); 31 | delete = ( 32 | ); 33 | update = ( 34 | ); 35 | } 36 | 2020-03-21 15:37:43.342040+0800 Runner[90808:13081752] flutter: {delete: [], create: [], update: []} 37 | 2020-03-21 15:37:43.342530+0800 Runner[90808:13081752] flutter: {delete: [], create: [], update: []} -------------------------------------------------------------------------------- /macos: -------------------------------------------------------------------------------- 1 | ios -------------------------------------------------------------------------------- /ohos/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /.preview 4 | /.idea 5 | /build 6 | /.cxx 7 | /.test 8 | /BuildProfile.ets 9 | /oh-package-lock.json5 10 | local.properties -------------------------------------------------------------------------------- /ohos/build-profile.json5: -------------------------------------------------------------------------------- 1 | { 2 | "apiType": "stageMode", 3 | "buildOption": { 4 | }, 5 | "targets": [ 6 | { 7 | "name": "default" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /ohos/hvigorfile.ts: -------------------------------------------------------------------------------- 1 | // Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. 2 | export { harTasks } from '@ohos/hvigor-ohos-plugin'; -------------------------------------------------------------------------------- /ohos/index.ets: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import PhotoManagerPlugin from './src/main/ets/components/plugin/PhotoManagerPlugin'; 17 | export default PhotoManagerPlugin; 18 | -------------------------------------------------------------------------------- /ohos/oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "name": "photo_manager", 3 | "version": "1.0.0", 4 | "description": "Please describe the basic information.", 5 | "main": "index.ets", 6 | "author": "", 7 | "license": "Apache-2.0", 8 | "dependencies": { 9 | "@ohos/flutter_ohos": "file:./har/flutter.har" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ohos/src/main/ets/components/plugin/handlers/HandlerBase.ets: -------------------------------------------------------------------------------- 1 | import { common } from '@kit.AbilityKit'; 2 | import { MethodCall } from '@ohos/flutter_ohos'; 3 | import { MethodResult } from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel'; 4 | import PhotoManagerPlugin from '../PhotoManagerPlugin'; 5 | 6 | export class HandlerBase { 7 | get uiContext(): common.UIAbilityContext | null { 8 | return PhotoManagerPlugin.uiContext; 9 | } 10 | 11 | get context(): common.Context | null { 12 | return PhotoManagerPlugin.uiContext; 13 | } 14 | 15 | get showLog(): boolean { 16 | return PhotoManagerPlugin.showLog; 17 | } 18 | 19 | async tryAgain(call: MethodCall, result: MethodResult, doWork: () => Promise, numberOfRetries: number = 1): Promise { 20 | if (numberOfRetries < 0) { 21 | return false; 22 | } 23 | try { 24 | return await doWork(); 25 | } 26 | catch (e) { 27 | if (this.showLog) { 28 | console.error(`${call.method} failed, code is ${e.code}, message is ${e.message}`); 29 | } 30 | if (numberOfRetries - 1 > 0) { 31 | return this.tryAgain(call, result, doWork, numberOfRetries - 1); 32 | } 33 | else { 34 | result.error(`${e.code}`, call.method, `${e.message}`); 35 | return true; 36 | } 37 | } 38 | } 39 | } 40 | 41 | 42 | export interface MethodCallHandlerBase { 43 | onMethodCall(call: MethodCall, result: MethodResult): Promise; 44 | } -------------------------------------------------------------------------------- /ohos/src/main/ets/components/plugin/types/RequestType.ets: -------------------------------------------------------------------------------- 1 | 2 | export class RequestType { 3 | constructor(value: number) { 4 | this.value = value; 5 | } 6 | value: number; 7 | static readonly _imageValue = 1; 8 | static readonly _videoValue = 1 << 1; 9 | static readonly _audioValue = 1 << 2; 10 | static readonly all = new RequestType( 11 | RequestType._imageValue | RequestType._videoValue | RequestType._audioValue 12 | ); 13 | static readonly common = new RequestType( 14 | RequestType._imageValue | RequestType._videoValue 15 | ); 16 | static readonly image = new RequestType(RequestType._imageValue); 17 | static readonly video = new RequestType(RequestType._videoValue); 18 | static readonly audio = new RequestType(RequestType._audioValue); 19 | 20 | containsImage(): boolean { 21 | return (this.value & RequestType._imageValue) === RequestType._imageValue; 22 | } 23 | 24 | containsVideo(): boolean { 25 | return (this.value & RequestType._videoValue) === RequestType._videoValue; 26 | } 27 | 28 | containsAudio(): boolean { 29 | return (this.value & RequestType._audioValue) === RequestType._audioValue; 30 | } 31 | 32 | containsType(type: RequestType): boolean { 33 | return (this.value & type.value) === type.value; 34 | } 35 | 36 | add(type: RequestType): RequestType { 37 | return new RequestType(this.value | type.value); 38 | } 39 | 40 | subtract(type: RequestType): RequestType { 41 | return new RequestType(this.value ^ type.value); 42 | } 43 | 44 | shiftRight(bit: number): RequestType { 45 | return new RequestType(this.value >> bit); 46 | } 47 | 48 | shiftLeft(bit: number): RequestType { 49 | return new RequestType(this.value << bit); 50 | } 51 | 52 | static values: RequestType[] = [RequestType.image, RequestType.video, RequestType.audio]; 53 | 54 | static fromTypes(types: RequestType[]): RequestType { 55 | let result = new RequestType(0); 56 | for (const type of types) { 57 | result = result.add(type); 58 | } 59 | return result; 60 | } 61 | 62 | equals(other: ESObject): boolean { 63 | return other instanceof RequestType && this.value === other.value; 64 | } 65 | 66 | hashCode(): number { 67 | return this.value; 68 | } 69 | 70 | toString(): string { 71 | return `${this.constructor.name}(${this.value})`; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ohos/src/main/ets/components/plugin/utils/EnumUtils.ets: -------------------------------------------------------------------------------- 1 | export class EnumUtils { 2 | static getName(enumType: ESObject, value: ESObject,): string { 3 | let keys = Object.keys(enumType) 4 | .filter(x => (enumType[x] === value)); 5 | let typeName: string = keys.length > 0 ? keys[0] : ''; 6 | return typeName; 7 | } 8 | } -------------------------------------------------------------------------------- /ohos/src/main/ets/components/plugin/utils/MapUtils.ets: -------------------------------------------------------------------------------- 1 | 2 | 3 | export class MapUtils { 4 | static get(map: Map, key: string, defaultValue: T): T { 5 | return map.has(key) ? map.get(key) as T : defaultValue; 6 | } 7 | } -------------------------------------------------------------------------------- /ohos/src/main/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "photo_manager", 4 | "type": "har", 5 | "deviceTypes": [ 6 | "default", 7 | "tablet" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: photo_manager 2 | description: A Flutter plugin that provides album assets abstraction management APIs on Android, iOS, macOS, and OpenHarmony. 3 | repository: https://github.com/fluttercandies/flutter_photo_manager 4 | version: 3.7.1 5 | 6 | environment: 7 | sdk: ">=2.13.0 <4.0.0" 8 | flutter: ">=2.2.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_lints: any 16 | flutter_test: 17 | sdk: flutter 18 | 19 | flutter: 20 | plugin: 21 | platforms: 22 | android: 23 | package: com.fluttercandies.photo_manager 24 | pluginClass: PhotoManagerPlugin 25 | ios: 26 | pluginClass: PhotoManagerPlugin 27 | macos: 28 | pluginClass: PhotoManagerPlugin 29 | ohos: 30 | pluginClass: PhotoManagerPlugin 31 | -------------------------------------------------------------------------------- /scripts/check_license.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | // ignore_for_file: avoid_print 6 | 7 | import 'dart:io'; 8 | 9 | const _license = 10 | '''// Copyright 2018 The FlutterCandies author. All rights reserved. 11 | // Use of this source code is governed by an Apache license that can be found 12 | // in the LICENSE file.'''; 13 | 14 | void main(List args) { 15 | final dir = Directory(args[0]); 16 | 17 | final dartFiles = dir 18 | .listSync(recursive: true) 19 | .whereType() 20 | .where((file) => file.path.endsWith('.dart')); 21 | 22 | final unlicensedFiles = []; 23 | 24 | for (final file in dartFiles) { 25 | final content = file.readAsStringSync(); 26 | final lines = content.split('\n'); 27 | final checkLines = lines.sublist(0, 3); 28 | 29 | if (checkLines.join('\n') != _license) { 30 | unlicensedFiles.add(file); 31 | } 32 | } 33 | 34 | if (unlicensedFiles.isNotEmpty) { 35 | print('The following files are missing the license header:'); 36 | for (final file in unlicensedFiles) { 37 | print(file.path); 38 | } 39 | exit(1); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/internal/extensions_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:photo_manager/photo_manager.dart'; 7 | 8 | void main() { 9 | test('Permission extensions equality test', () async { 10 | const PermissionState permission = PermissionState.limited; 11 | expect( 12 | permission.isAuth == (permission == PermissionState.authorized), 13 | equals(true), 14 | ); 15 | expect( 16 | permission.hasAccess == 17 | (permission == PermissionState.authorized || 18 | permission == PermissionState.limited), 19 | equals(true), 20 | ); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/photo_manager_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The FlutterCandies author. All rights reserved. 2 | // Use of this source code is governed by an Apache license that can be found 3 | // in the LICENSE file. 4 | 5 | // ignore_for_file: use_named_constants 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:photo_manager/photo_manager.dart'; 8 | 9 | class _TestPlugin extends PhotoManagerPlugin { 10 | @override 11 | Future requestPermissionExtend(_) { 12 | return Future.value(PermissionState.notDetermined); 13 | } 14 | } 15 | 16 | void main() { 17 | test('RequestType equality test', () { 18 | expect(RequestType.image == const RequestType(1), equals(true)); 19 | expect(RequestType.video == const RequestType(2), equals(true)); 20 | expect(RequestType.audio == const RequestType(4), equals(true)); 21 | expect(RequestType.common == const RequestType(3), equals(true)); 22 | expect(RequestType.all == const RequestType(7), equals(true)); 23 | }); 24 | 25 | test('Construct custom plugin', () async { 26 | final _TestPlugin testPlugin = _TestPlugin(); 27 | PhotoManager.withPlugin(testPlugin); 28 | final PermissionState permission = 29 | await PhotoManager.requestPermissionExtend(); 30 | expect(permission == PermissionState.notDetermined, equals(true)); 31 | }); 32 | } 33 | --------------------------------------------------------------------------------