├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── ImageGallerySaverPlugin.h │ ├── ImageGallerySaverPlugin.m │ └── SwiftImageGallerySaverPlugin.swift ├── .gitignore └── image_gallery_saver.podspec ├── android ├── settings.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── imagegallerysaver │ │ └── ImageGallerySaverPlugin.kt ├── build.gradle ├── gradlew.bat └── gradlew ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── 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-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ └── Podfile ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ ├── 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 │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── test │ └── widget_test.dart ├── lib │ ├── style.dart │ ├── utils.dart │ ├── main.dart │ └── dialog.dart ├── .gitignore ├── README.md └── pubspec.yaml ├── .gitignore ├── .metadata ├── .travis.yaml ├── .travis.yml ├── pubspec.yaml ├── LICENSE ├── test └── image_gallery_saver_test.dart ├── lib └── image_gallery_saver.dart ├── CHANGELOG.md └── README.md /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'image_gallery_saver' 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hui-z/image_gallery_saver/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /ios/Classes/ImageGallerySaverPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ImageGallerySaverPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | .idea/ 7 | .fvm/ 8 | 9 | pubspec.lock 10 | 11 | build/ 12 | 13 | *.iml -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hui-z/image_gallery_saver/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/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/hui-z/image_gallery_saver/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hui-z/image_gallery_saver/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hui-z/image_gallery_saver/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hui-z/image_gallery_saver/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hui-z/image_gallery_saver/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Sep 09 11:20:21 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 10 12:21:25 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.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.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 6a3ff018b199a7febbe2b5adbb564081d8f49e2f 8 | channel: dev 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 6a3ff018b199a7febbe2b5adbb564081d8f49e2f 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /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/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 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 | -------------------------------------------------------------------------------- /ios/Classes/ImageGallerySaverPlugin.m: -------------------------------------------------------------------------------- 1 | #import "ImageGallerySaverPlugin.h" 2 | 3 | #if __has_include() 4 | #import 5 | #else 6 | #import "image_gallery_saver-Swift.h" 7 | #endif 8 | 9 | @implementation ImageGallerySaverPlugin 10 | + (void)registerWithRegistrar:(NSObject*)registrar { 11 | [SwiftImageGallerySaverPlugin registerWithRegistrar:registrar]; 12 | } 13 | @end 14 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:image_gallery_saver_example/main.dart'; 4 | 5 | void main() { 6 | testWidgets('Verify Widgets', (WidgetTester tester) async { 7 | // Build our app and trigger a frame. 8 | await tester.pumpWidget(new MyApp()); 9 | final Finder flatButtonPass = find.widgetWithText(ElevatedButton, '保存屏幕截图'); 10 | expect(flatButtonPass, findsOneWidget); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.travis.yaml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: false 4 | addons: 5 | apt: 6 | # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 7 | sources: 8 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 9 | packages: 10 | - libstdc++6 11 | - fonts-droid-fallback 12 | before_script: 13 | - git clone https://github.com/flutter/flutter.git -b stable --depth 1 14 | - ./flutter/bin/flutter doctor 15 | script: 16 | - ./flutter/bin/flutter test 17 | cache: 18 | directories: 19 | - $HOME/.pub-cache 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: false 4 | addons: 5 | apt: 6 | # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 7 | sources: 8 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 9 | packages: 10 | - libstdc++6 11 | - fonts-droid-fallback 12 | before_script: 13 | - git clone https://github.com/flutter/flutter.git -b stable --depth 1 14 | - ./flutter/bin/flutter doctor 15 | script: 16 | - ./flutter/bin/flutter test 17 | cache: 18 | directories: 19 | - $HOME/.pub-cache 20 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 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/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: image_gallery_saver 2 | description: A flutter plugin for save image to gallery, iOS need to add the following keys to your Info.plist file. 3 | version: 2.0.3 4 | homepage: https://github.com/hui-z/image_gallery_saver 5 | 6 | environment: 7 | sdk: '>=2.12.0 <4.0.0' 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | 15 | # For information on the generic Dart part of this file, see the 16 | # following page: https://www.dartlang.org/tools/pub/pubspec 17 | 18 | # The following section is specific to Flutter. 19 | flutter: 20 | plugin: 21 | platforms: 22 | android: 23 | package: com.example.imagegallerysaver 24 | pluginClass: ImageGallerySaverPlugin 25 | ios: 26 | pluginClass: ImageGallerySaverPlugin 27 | 28 | dev_dependencies: 29 | flutter_test: 30 | sdk: flutter 31 | 32 | -------------------------------------------------------------------------------- /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 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/image_gallery_saver.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'image_gallery_saver' 6 | s.version = '2.0.2' 7 | s.summary = 'A new flutter plugin project.' 8 | s.description = <<-DESC 9 | A new flutter plugin project. 10 | DESC 11 | s.homepage = 'http://example.com' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Your Company' => 'email@example.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | 19 | s.ios.deployment_target = '8.0' 20 | s.swift_version = '5.0' 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 22 | end 23 | 24 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.example.imagegallerysaver' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.7.10' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.3.0' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | compileSdkVersion 30 29 | 30 | sourceSets { 31 | main.java.srcDirs += 'src/main/kotlin' 32 | } 33 | defaultConfig { 34 | minSdkVersion 16 35 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 36 | } 37 | lintOptions { 38 | disable 'InvalidPackage' 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [zaihui] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/lib/style.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class HFontSizes { 6 | static const normal = 16.0; 7 | static const normal2 = 15.0; 8 | static const small = 14.0; 9 | static const smaller = 13.0; 10 | } 11 | 12 | extension FontWeightExt on FontWeight { 13 | static const FontWeight light = FontWeight.w300; 14 | static const FontWeight regular = FontWeight.w400; 15 | static FontWeight medium = Platform.isAndroid ? FontWeight.w600 : FontWeight.w600; 16 | static const FontWeight semibold = FontWeight.w800; 17 | static const FontWeight bold = FontWeight.w900; 18 | } 19 | 20 | class HColors { 21 | static const Color contentBackgroundColor = Colors.white; 22 | static const Color contentBackgroundGrayColor2 = Color(0xFFF8F8F8); 23 | static const Color secondaryLabelColor2 = Color(0xFF666666); 24 | static const Color secondaryLabelColor3 = Color(0xFF999999); 25 | static const Color secondaryLabelColor4 = Color(0xFFB4B4B4); 26 | static const Color blue2 = Color(0xFF0091FF); 27 | static const Color primary = Color(0xFF448AFF); 28 | static const Color secondary = Color(0xFFE95E3D); 29 | static const Color mediumGrey = Color(0xFF86919C); 30 | } 31 | -------------------------------------------------------------------------------- /test/image_gallery_saver_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:image_gallery_saver/image_gallery_saver.dart'; 6 | 7 | void main() { 8 | TestWidgetsFlutterBinding.ensureInitialized(); 9 | 10 | const MethodChannel channel = MethodChannel('image_gallery_saver'); 11 | final List log = []; 12 | bool? response; 13 | 14 | channel.setMockMethodCallHandler((MethodCall methodCall) async { 15 | log.add(methodCall); 16 | return response; 17 | }); 18 | 19 | tearDown(() { 20 | log.clear(); 21 | }); 22 | 23 | 24 | test('saveImageToGallery test', () async { 25 | response = true; 26 | Uint8List imageBytes = Uint8List(16); 27 | final bool? result = await (ImageGallerySaver.saveImage(imageBytes) as FutureOr); 28 | expect( 29 | log, 30 | [ 31 | isMethodCall('saveImageToGallery', arguments: { 32 | 'imageBytes': imageBytes, 33 | 'quality': 80, 34 | 'name': null, 35 | "isReturnImagePathOfIOS": false 36 | }) 37 | ], 38 | ); 39 | expect(result, response); 40 | }); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /lib/image_gallery_saver.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/services.dart'; 4 | 5 | class ImageGallerySaver { 6 | static const MethodChannel _channel = 7 | const MethodChannel('image_gallery_saver'); 8 | 9 | /// save image to Gallery 10 | /// imageBytes can't null 11 | /// return Map type 12 | /// for example:{"isSuccess":true, "filePath":String?} 13 | static FutureOr saveImage(Uint8List imageBytes, 14 | {int quality = 80, 15 | String? name, 16 | bool isReturnImagePathOfIOS = false}) async { 17 | final result = 18 | await _channel.invokeMethod('saveImageToGallery', { 19 | 'imageBytes': imageBytes, 20 | 'quality': quality, 21 | 'name': name, 22 | 'isReturnImagePathOfIOS': isReturnImagePathOfIOS 23 | }); 24 | return result; 25 | } 26 | 27 | /// Save the PNG,JPG,JPEG image or video located at [file] to the local device media gallery. 28 | static Future saveFile(String file, 29 | {String? name, bool isReturnPathOfIOS = false}) async { 30 | final result = await _channel.invokeMethod( 31 | 'saveFileToGallery', { 32 | 'file': file, 33 | 'name': name, 34 | 'isReturnPathOfIOS': isReturnPathOfIOS 35 | }); 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # source '源地址' 5 | source 'https://github.com/CocoaPods/Specs.git' 6 | 7 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 8 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 9 | 10 | project 'Runner', { 11 | 'Debug' => :debug, 12 | 'Profile' => :release, 13 | 'Release' => :release, 14 | } 15 | 16 | def flutter_root 17 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 18 | unless File.exist?(generated_xcode_build_settings_path) 19 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 20 | end 21 | 22 | File.foreach(generated_xcode_build_settings_path) do |line| 23 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 24 | return matches[1].strip if matches 25 | end 26 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 27 | end 28 | 29 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 30 | 31 | flutter_ios_podfile_setup 32 | 33 | target 'Runner' do 34 | use_frameworks! 35 | use_modular_headers! 36 | 37 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .flutter-plugins-dependencies 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # Visual Studio Code related 21 | .vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/Flutter/flutter_export_environment.sh 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | -------------------------------------------------------------------------------- /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/README.md: -------------------------------------------------------------------------------- 1 | example/lib/main.dart 2 | 3 | ```dart 4 | _saveLocalImage() async { 5 | RenderRepaintBoundary boundary = 6 | _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; 7 | ui.Image image = await boundary.toImage(); 8 | ByteData? byteData = 9 | await (image.toByteData(format: ui.ImageByteFormat.png)); 10 | if (byteData != null) { 11 | final result = 12 | await ImageGallerySaver.saveImage(byteData.buffer.asUint8List()); 13 | print(result); 14 | Utils.toast(result.toString()); 15 | } 16 | } 17 | 18 | _saveNetworkImage() async { 19 | var response = await Dio().get( 20 | "https://ss0.baidu.com/94o3dSag_xI4khGko9WTAnF6hhy/image/h%3D300/sign=a62e824376d98d1069d40a31113eb807/838ba61ea8d3fd1fc9c7b6853a4e251f94ca5f46.jpg", 21 | options: Options(responseType: ResponseType.bytes)); 22 | final result = await ImageGallerySaver.saveImage( 23 | Uint8List.fromList(response.data), 24 | quality: 60, 25 | name: "hello"); 26 | print(result); 27 | Utils.toast("$result"); 28 | } 29 | 30 | _saveNetworkGifFile() async { 31 | var appDocDir = await getTemporaryDirectory(); 32 | String savePath = appDocDir.path + "/temp.gif"; 33 | String fileUrl = 34 | "https://hyjdoc.oss-cn-beijing.aliyuncs.com/hyj-doc-flutter-demo-run.gif"; 35 | await Dio().download(fileUrl, savePath); 36 | final result = 37 | await ImageGallerySaver.saveFile(savePath, isReturnPathOfIOS: true); 38 | print(result); 39 | Utils.toast("$result"); 40 | } 41 | 42 | _saveNetworkVideoFile() async { 43 | var appDocDir = await getTemporaryDirectory(); 44 | String savePath = appDocDir.path + "/temp.mp4"; 45 | String fileUrl = 46 | "https://s3.cn-north-1.amazonaws.com.cn/mtab.kezaihui.com/video/ForBiggerBlazes.mp4"; 47 | await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) { 48 | print((count / total * 100).toStringAsFixed(0) + "%"); 49 | }); 50 | final result = await ImageGallerySaver.saveFile(savePath); 51 | print(result); 52 | Utils.toast("$result"); 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | image_gallery_saver_example 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | sandbox_fox_example 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NSPhotoLibraryAddUsageDescription 30 | 31 | NSPhotoLibraryUsageDescription 32 | 获取相册权限 33 | UIApplicationSupportsIndirectInputEvents 34 | 35 | UILaunchStoryboardName 36 | LaunchScreen 37 | UIMainStoryboardFile 38 | Main 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.3 2 | - 1.Upgrade flutter version to 3.10.5 3 | - 2.Android build tools are upgraded to 7.3.0 4 | - 3.Optimize the Android plugin library code 5 | 6 | ## 2.0.2 7 | - 1.Optimization android plugin 8 | 9 | ## 2.0.1 10 | - 1.Upgrade flutter version to 3.10.2 11 | - 2.Upgrade Android/ios plug-in related 12 | - 3.Support Android 13 13 | - 4.Support ios16 14 | 15 | ## 1.7.1 16 | - optimization 17 | 18 | ## 1.7.0 19 | - optimization 20 | 21 | ## 1.6.9 22 | - optimization 23 | 24 | ## 1.6.8 25 | - Support android 11 save 26 | 27 | ## 1.6.7 28 | - fix ios bug 29 | 30 | ## 1.6.6 31 | * fix ios bug 32 | 33 | ## 1.6.5 34 | * fix android bug 35 | 36 | ## 1.6.4 37 | * formatted code 38 | 39 | ## 1.6.3 40 | * Save result return more message 41 | 42 | ## 1.6.2 43 | * fix crash on iOS when granting permission 44 | 45 | ## 1.6.1 46 | * fix iOS Swift5.1 error 47 | 48 | ## 1.6.0 49 | * Support iOS return save path 50 | 51 | ## 1.5.0 52 | * Save image with JPG and Support special quality(ios & Android) 53 | * Support special Image name for Android 54 | * Upgrade libraries and dependence 55 | * fix docs 56 | * Add more example 57 | 58 | ## 1.3.0 59 | 60 | * Define clang module for static ios builds 61 | * Cleanup example project 62 | 63 | ## 1.2.2 64 | 65 | * Migrate to AndroidX 66 | * optimize git ignore 67 | 68 | ## 1.2.1 69 | 70 | * Support return path for Android. 71 | * Fix bug(save video fail for Android). 72 | 73 | ## 1.2.0 74 | 75 | * Support video save and file path to gallery 76 | * Add example for save video and net image 77 | 78 | ## 1.1.0 79 | 80 | * Upgrade kotlin(1.3.20) and gradle build plugin version(3.3.0). 81 | 82 | ## 1.0.0 83 | 84 | * Updated Kotlin Gradle plugin version 85 | 86 | ## 0.1.2 87 | 88 | * Remove hard coded path - image_gallery_saver in Android 89 | 90 | ## 0.1.1 91 | 92 | * Updated README and Description 93 | 94 | ## 0.1.0 95 | 96 | * Updated README and CHANGELOG 97 | * Add LICENSE 98 | * Add Test 99 | 100 | ## 0.0.2 101 | 102 | * Updated README and CHANGELOG 103 | 104 | ## 0.0.1 105 | 106 | * Initial Open Source release. 107 | -------------------------------------------------------------------------------- /example/lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:fluttertoast/fluttertoast.dart'; 5 | import 'package:permission_handler/permission_handler.dart'; 6 | 7 | import 'dialog.dart'; 8 | 9 | class Utils { 10 | static void toast(String msg) { 11 | Fluttertoast.showToast( 12 | msg: msg, 13 | toastLength: Toast.LENGTH_SHORT, 14 | gravity: ToastGravity.CENTER, 15 | timeInSecForIosWeb: 1, 16 | textColor: Colors.white, 17 | fontSize: 16.0, 18 | backgroundColor: Colors.black, 19 | ); 20 | } 21 | } 22 | 23 | class PermissionUtil { 24 | /// 安卓权限 25 | static List androidPermissions = [ 26 | // 在这里添加需要的权限 27 | Permission.storage 28 | ]; 29 | 30 | /// ios权限 31 | static List iosPermissions = [ 32 | // 在这里添加需要的权限 33 | Permission.storage 34 | ]; 35 | 36 | static Future> requestAll() async { 37 | if (Platform.isIOS) { 38 | return await iosPermissions.request(); 39 | } 40 | return await androidPermissions.request(); 41 | } 42 | 43 | static Future> request( 44 | Permission permission) async { 45 | final List permissions = [permission]; 46 | return await permissions.request(); 47 | } 48 | 49 | static bool isDenied(Map result) { 50 | var isDenied = false; 51 | result.forEach((key, value) { 52 | if (value == PermissionStatus.denied) { 53 | isDenied = true; 54 | return; 55 | } 56 | }); 57 | return isDenied; 58 | } 59 | 60 | static void showDeniedDialog(BuildContext context) { 61 | HDialog.show( 62 | context: context, 63 | title: '权限申请异常', 64 | content: '请在【应用信息】-【权限管理】中,开启全部所需权限,以正常使用惠爆单功能', 65 | options: [ 66 | DialogAction(text: '去设置', onPressed: () => openAppSettings()) 67 | ]); 68 | } 69 | 70 | /// 检查权限 71 | static Future checkGranted(Permission permission) async { 72 | PermissionStatus storageStatus = await permission.status; 73 | if (storageStatus == PermissionStatus.granted) { 74 | //已授权 75 | return true; 76 | } else { 77 | //拒绝授权 78 | return false; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | lintOptions { 46 | disable 'InvalidPackage' 47 | } 48 | 49 | defaultConfig { 50 | applicationId "com.example.imagegallerysaverexample" 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion flutter.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 56 | } 57 | 58 | buildTypes { 59 | release { 60 | // TODO: Add your own signing config for the release build. 61 | // Signing with the debug keys for now, so `flutter run --release` works. 62 | signingConfig signingConfigs.debug 63 | } 64 | } 65 | } 66 | 67 | flutter { 68 | source '../..' 69 | } 70 | 71 | dependencies { 72 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 73 | testImplementation 'junit:junit:4.12' 74 | androidTestImplementation 'androidx.test:runner:1.2.0' 75 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 76 | } 77 | -------------------------------------------------------------------------------- /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/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 17 | 22 | 30 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle 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 Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: image_gallery_saver_example 2 | description: Demonstrates how to use the image_gallery_saver plugin. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 2.0.3+3 11 | 12 | environment: 13 | sdk: '>=2.19.6 <4.0.0' 14 | 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | 19 | permission_handler: ^10.3.0 20 | fluttertoast: ^8.2.2 21 | path_provider: ^2.0.15 22 | dio: ^5.2.1+1 23 | 24 | image_gallery_saver: 25 | path: ../ 26 | cupertino_icons: ^1.0.5 27 | 28 | dev_dependencies: 29 | flutter_test: 30 | sdk: flutter 31 | flutter_lints: ^2.0.1 32 | 33 | # For information on the generic Dart part of this file, see the 34 | # following page: https://www.dartlang.org/tools/pub/pubspec 35 | 36 | # The following section is specific to Flutter. 37 | flutter: 38 | 39 | # The following line ensures that the Material Icons font is 40 | # included with your application, so that you can use the icons in 41 | # the material Icons class. 42 | uses-material-design: true 43 | 44 | # To add assets to your application, add an assets section, like this: 45 | # assets: 46 | # - images/a_dot_burr.jpeg 47 | # - images/a_dot_ham.jpeg 48 | 49 | # An image asset can refer to one or more resolution-specific "variants", see 50 | # https://flutter.io/assets-and-images/#resolution-aware. 51 | 52 | # For details regarding adding assets from package dependencies, see 53 | # https://flutter.io/assets-and-images/#from-packages 54 | 55 | # To add custom fonts to your application, add a fonts section here, 56 | # in this "flutter" section. Each entry in this list should have a 57 | # "family" key with the font family name, and a "fonts" key with a 58 | # list giving the asset and other descriptors for the font. For 59 | # example: 60 | # fonts: 61 | # - family: Schyler 62 | # fonts: 63 | # - asset: fonts/Schyler-Regular.ttf 64 | # - asset: fonts/Schyler-Italic.ttf 65 | # style: italic 66 | # - family: Trajan Pro 67 | # fonts: 68 | # - asset: fonts/TrajanPro.ttf 69 | # - asset: fonts/TrajanPro_Bold.ttf 70 | # weight: 700 71 | # 72 | # For details regarding fonts from package dependencies, 73 | # see https://flutter.io/custom-fonts/#from-packages 74 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # image_gallery_saver 2 | 3 | [![Build Status](https://travis-ci.org/hui-z/image_gallery_saver.svg?branch=master)](https://travis-ci.org/hui-z/image_gallery_saver#) 4 | [![pub package](https://img.shields.io/pub/v/image_gallery_saver.svg)](https://pub.dartlang.org/packages/image_gallery_saver) 5 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://choosealicense.com/licenses/mit/) 6 | 7 | We use the `image_picker` plugin to select images from the Android and iOS image library, but it can't save images to the gallery. This plugin can provide this feature. 8 | 9 | ## Usage 10 | 11 | To use this plugin, add `image_gallery_saver` as a dependency in your pubspec.yaml file. For example: 12 | ```yaml 13 | dependencies: 14 | image_gallery_saver: '^2.0.3' 15 | ``` 16 | 17 | ## iOS 18 | Your project need create with swift. 19 | Add the following keys to your Info.plist file, located in /ios/Runner/Info.plist: 20 | * NSPhotoLibraryAddUsageDescription - describe why your app needs permission for the photo library. This is called Privacy - Photo Library Additions Usage Description in the visual editor 21 | 22 | ## Android 23 | You need to ask for storage permission to save an image to the gallery. You can handle the storage permission using [flutter_permission_handler](https://github.com/BaseflowIT/flutter-permission-handler). 24 | In Android version 10, Open the manifest file and add this line to your application tag 25 | ``` 26 | 27 | ``` 28 | 29 | ## Example 30 | Saving an image from the internet, quality and name is option 31 | ``` dart 32 | _saveLocalImage() async { 33 | RenderRepaintBoundary boundary = 34 | _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; 35 | ui.Image image = await boundary.toImage(); 36 | ByteData? byteData = 37 | await (image.toByteData(format: ui.ImageByteFormat.png)); 38 | if (byteData != null) { 39 | final result = 40 | await ImageGallerySaver.saveImage(byteData.buffer.asUint8List()); 41 | print(result); 42 | } 43 | } 44 | 45 | _saveNetworkImage() async { 46 | var response = await Dio().get( 47 | "https://ss0.baidu.com/94o3dSag_xI4khGko9WTAnF6hhy/image/h%3D300/sign=a62e824376d98d1069d40a31113eb807/838ba61ea8d3fd1fc9c7b6853a4e251f94ca5f46.jpg", 48 | options: Options(responseType: ResponseType.bytes)); 49 | final result = await ImageGallerySaver.saveImage( 50 | Uint8List.fromList(response.data), 51 | quality: 60, 52 | name: "hello"); 53 | print(result); 54 | } 55 | ``` 56 | 57 | Saving file(ig: video/gif/others) from the internet 58 | ``` dart 59 | _saveNetworkGifFile() async { 60 | var appDocDir = await getTemporaryDirectory(); 61 | String savePath = appDocDir.path + "/temp.gif"; 62 | String fileUrl = 63 | "https://hyjdoc.oss-cn-beijing.aliyuncs.com/hyj-doc-flutter-demo-run.gif"; 64 | await Dio().download(fileUrl, savePath); 65 | final result = 66 | await ImageGallerySaver.saveFile(savePath, isReturnPathOfIOS: true); 67 | print(result); 68 | } 69 | 70 | _saveNetworkVideoFile() async { 71 | var appDocDir = await getTemporaryDirectory(); 72 | String savePath = appDocDir.path + "/temp.mp4"; 73 | String fileUrl = 74 | "https://s3.cn-north-1.amazonaws.com.cn/mtab.kezaihui.com/video/ForBiggerBlazes.mp4"; 75 | await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) { 76 | print((count / total * 100).toStringAsFixed(0) + "%"); 77 | }); 78 | final result = await ImageGallerySaver.saveFile(savePath); 79 | print(result); 80 | } 81 | ``` 82 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | import 'package:image_gallery_saver/image_gallery_saver.dart'; 8 | import 'package:path_provider/path_provider.dart'; 9 | 10 | import 'utils.dart'; 11 | 12 | void main() => runApp(MyApp()); 13 | 14 | class MyApp extends StatelessWidget { 15 | @override 16 | Widget build(BuildContext context) { 17 | return MaterialApp( 18 | title: 'Save image to gallery', 19 | theme: ThemeData( 20 | primarySwatch: Colors.blue, 21 | ), 22 | home: MyHomePage(), 23 | ); 24 | } 25 | } 26 | 27 | class MyHomePage extends StatefulWidget { 28 | @override 29 | _MyHomePageState createState() => _MyHomePageState(); 30 | } 31 | 32 | class _MyHomePageState extends State { 33 | GlobalKey _globalKey = GlobalKey(); 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | PermissionUtil.requestAll(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return Scaffold( 44 | appBar: AppBar( 45 | title: Text("Save image to gallery"), 46 | ), 47 | body: Center( 48 | child: Column( 49 | children: [ 50 | SizedBox(height: 15), 51 | RepaintBoundary( 52 | key: _globalKey, 53 | child: Container( 54 | alignment: Alignment.center, 55 | width: 300, 56 | height: 300, 57 | color: Colors.blue, 58 | ), 59 | ), 60 | Container( 61 | padding: EdgeInsets.only(top: 15), 62 | child: ElevatedButton( 63 | onPressed: _saveLocalImage, 64 | child: Text("Save Local Image"), 65 | ), 66 | width: 300, 67 | height: 44, 68 | ), 69 | Container( 70 | padding: EdgeInsets.only(top: 15), 71 | child: ElevatedButton( 72 | onPressed: _saveNetworkImage, 73 | child: Text("Save Network Image"), 74 | ), 75 | width: 300, 76 | height: 44, 77 | ), 78 | Container( 79 | padding: EdgeInsets.only(top: 15), 80 | child: ElevatedButton( 81 | onPressed: _saveNetworkGifFile, 82 | child: Text("Save Network Gif Image"), 83 | ), 84 | width: 300, 85 | height: 44, 86 | ), 87 | Container( 88 | padding: EdgeInsets.only(top: 15), 89 | child: ElevatedButton( 90 | onPressed: _saveNetworkVideoFile, 91 | child: Text("Save Network Video"), 92 | ), 93 | width: 300, 94 | height: 44, 95 | ), 96 | ], 97 | ), 98 | )); 99 | } 100 | 101 | _saveLocalImage() async { 102 | RenderRepaintBoundary boundary = 103 | _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; 104 | ui.Image image = await boundary.toImage(); 105 | ByteData? byteData = 106 | await (image.toByteData(format: ui.ImageByteFormat.png)); 107 | if (byteData != null) { 108 | final result = 109 | await ImageGallerySaver.saveImage(byteData.buffer.asUint8List()); 110 | print(result); 111 | Utils.toast(result.toString()); 112 | } 113 | } 114 | 115 | _saveNetworkImage() async { 116 | var response = await Dio().get( 117 | "https://ss0.baidu.com/94o3dSag_xI4khGko9WTAnF6hhy/image/h%3D300/sign=a62e824376d98d1069d40a31113eb807/838ba61ea8d3fd1fc9c7b6853a4e251f94ca5f46.jpg", 118 | options: Options(responseType: ResponseType.bytes)); 119 | final result = await ImageGallerySaver.saveImage( 120 | Uint8List.fromList(response.data), 121 | quality: 60, 122 | name: "hello"); 123 | print(result); 124 | Utils.toast("$result"); 125 | } 126 | 127 | _saveNetworkGifFile() async { 128 | var appDocDir = await getTemporaryDirectory(); 129 | String savePath = appDocDir.path + "/temp.gif"; 130 | String fileUrl = 131 | "https://hyjdoc.oss-cn-beijing.aliyuncs.com/hyj-doc-flutter-demo-run.gif"; 132 | await Dio().download(fileUrl, savePath); 133 | final result = 134 | await ImageGallerySaver.saveFile(savePath, isReturnPathOfIOS: true); 135 | print(result); 136 | Utils.toast("$result"); 137 | } 138 | 139 | _saveNetworkVideoFile() async { 140 | var appDocDir = await getTemporaryDirectory(); 141 | String savePath = appDocDir.path + "/temp.mp4"; 142 | String fileUrl = 143 | "https://s3.cn-north-1.amazonaws.com.cn/mtab.kezaihui.com/video/ForBiggerBlazes.mp4"; 144 | await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) { 145 | print((count / total * 100).toStringAsFixed(0) + "%"); 146 | }); 147 | final result = await ImageGallerySaver.saveFile(savePath); 148 | print(result); 149 | Utils.toast("$result"); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /example/lib/dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | import 'style.dart'; 8 | 9 | class HDialog { 10 | static Future show( 11 | {required BuildContext context, 12 | String title = '温馨提示', 13 | String? content, 14 | bool barrierDismissible = true, 15 | Color? barrierColor = Colors.black54, 16 | bool? useSafeArea, 17 | bool? useRootNavigator, 18 | Widget? contentWidget, 19 | List options = const [DialogAction(text: '知道了')]}) { 20 | return showDialog( 21 | context: context, 22 | barrierDismissible: barrierDismissible, 23 | barrierColor: barrierColor, 24 | useSafeArea: useSafeArea ?? true, 25 | useRootNavigator: useRootNavigator ?? true, 26 | builder: (BuildContext context) => HDialogWidget( 27 | title: title, 28 | content: content, 29 | contentWidget: contentWidget, 30 | options: options, 31 | isSheetStyle: false, 32 | )); 33 | } 34 | } 35 | 36 | class DialogAction { 37 | final String? text; 38 | final ActionType type; 39 | final Widget? child; 40 | final T? actionValue; 41 | final VoidCallback? onPressed; 42 | 43 | const DialogAction( 44 | {this.text, 45 | this.type = ActionType.positive, 46 | this.child, 47 | this.actionValue, 48 | this.onPressed}); 49 | } 50 | 51 | enum ActionType { delete, positive, negative } 52 | 53 | Color _getActionColor(ActionType type) { 54 | switch (type) { 55 | case ActionType.delete: 56 | return HColors.secondary; 57 | case ActionType.negative: 58 | return HColors.mediumGrey; 59 | case ActionType.positive: 60 | return HColors.primary; 61 | default: 62 | return HColors.primary; 63 | } 64 | } 65 | 66 | class HDialogWidget extends StatelessWidget { 67 | final String? title; 68 | final String? content; 69 | final Widget? contentWidget; 70 | final List> options; 71 | final DialogAction? bottomSheetCancel; 72 | final bool isSheetStyle; 73 | 74 | const HDialogWidget( 75 | {Key? key, 76 | this.title, 77 | this.content, 78 | this.contentWidget, 79 | this.options = const [], 80 | this.bottomSheetCancel, 81 | this.isSheetStyle = false}) 82 | : super(key: key); 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | if (Platform.isIOS) { 87 | return createIOSDialog( 88 | content: content, 89 | contentWidget: contentWidget, 90 | title: title, 91 | context: context, 92 | options: options); 93 | } else { 94 | return createAndroidDialog( 95 | content: content, 96 | contentWidget: contentWidget, 97 | title: title, 98 | context: context, 99 | options: options); 100 | } 101 | } 102 | 103 | AlertDialog createAndroidDialog( 104 | {required BuildContext context, 105 | String? title, 106 | String? content, 107 | required List> options, 108 | Widget? contentWidget}) { 109 | final actions = options.map((option) { 110 | return CupertinoButton( 111 | onPressed: () { 112 | Navigator.pop(context, option.actionValue); 113 | if (option.onPressed != null) option.onPressed!(); 114 | }, 115 | child: option.child ?? 116 | Text(option.text ?? '', 117 | style: TextStyle(color: _getActionColor(option.type)))); 118 | }).toList(); 119 | final dialog = AlertDialog( 120 | title: title==null ? null : Text(title), 121 | content: contentWidget != null && content==null 122 | ? contentWidget 123 | : Text(content ?? ''), 124 | actions: actions, 125 | ); 126 | return dialog; 127 | } 128 | 129 | Widget createIOSDialog( 130 | {required BuildContext context, 131 | String? title, 132 | String? content, 133 | Widget? contentWidget, 134 | required List> options}) { 135 | final actions = options.map((option) { 136 | return CupertinoButton( 137 | onPressed: () { 138 | Navigator.pop(context, option.actionValue); 139 | if (option.onPressed != null) option.onPressed!(); 140 | }, 141 | child: option.child ?? 142 | Text(option.text ?? '', 143 | style: TextStyle(color: _getActionColor(option.type))), 144 | ); 145 | }).toList(); 146 | return CupertinoAlertDialog( 147 | title: title==null ? null : Text(title), 148 | content: contentWidget != null && content == null 149 | ? contentWidget 150 | : Text(content ?? ''), 151 | actions: actions, 152 | ); 153 | } 154 | 155 | Widget createIOSSheetDialog( 156 | {required BuildContext context, 157 | String? title, 158 | String? content, 159 | Widget? contentWidget, 160 | required List> options, 161 | DialogAction? bottomSheetCancel}) { 162 | final actions = options.map((option) { 163 | return CupertinoActionSheetAction( 164 | onPressed: () { 165 | Navigator.pop(context, option.actionValue); 166 | if (option.onPressed != null) option.onPressed!(); 167 | }, 168 | child: option.child ?? 169 | Text(option.text ?? '', 170 | style: TextStyle(color: _getActionColor(option.type)))); 171 | }).toList(); 172 | final cancelButton = bottomSheetCancel == null 173 | ? null 174 | : CupertinoActionSheetAction( 175 | onPressed: () { 176 | Navigator.pop(context, bottomSheetCancel.actionValue); 177 | if (bottomSheetCancel.onPressed != null) 178 | bottomSheetCancel.onPressed!(); 179 | }, 180 | isDefaultAction: true, 181 | child: bottomSheetCancel.child ?? 182 | Text(bottomSheetCancel.text ?? '', 183 | style: TextStyle( 184 | color: _getActionColor(bottomSheetCancel.type))), 185 | ); 186 | return CupertinoActionSheet( 187 | title: title==null ? null : Text(title), 188 | message: contentWidget != null && content==null 189 | ? contentWidget 190 | : Text(content ?? ''), 191 | actions: actions, 192 | cancelButton: cancelButton); 193 | } 194 | } -------------------------------------------------------------------------------- /ios/Classes/SwiftImageGallerySaverPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import Photos 4 | 5 | public class SwiftImageGallerySaverPlugin: NSObject, FlutterPlugin { 6 | let errorMessage = "Failed to save, please check whether the permission is enabled" 7 | 8 | var result: FlutterResult?; 9 | 10 | public static func register(with registrar: FlutterPluginRegistrar) { 11 | let channel = FlutterMethodChannel(name: "image_gallery_saver", binaryMessenger: registrar.messenger()) 12 | let instance = SwiftImageGallerySaverPlugin() 13 | registrar.addMethodCallDelegate(instance, channel: channel) 14 | } 15 | 16 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 17 | self.result = result 18 | if call.method == "saveImageToGallery" { 19 | let arguments = call.arguments as? [String: Any] ?? [String: Any]() 20 | guard let imageData = (arguments["imageBytes"] as? FlutterStandardTypedData)?.data, 21 | let image = UIImage(data: imageData), 22 | let quality = arguments["quality"] as? Int, 23 | let _ = arguments["name"], 24 | let isReturnImagePath = arguments["isReturnImagePathOfIOS"] as? Bool 25 | else { return } 26 | let newImage = image.jpegData(compressionQuality: CGFloat(quality / 100))! 27 | saveImage(UIImage(data: newImage) ?? image, isReturnImagePath: isReturnImagePath) 28 | } else if (call.method == "saveFileToGallery") { 29 | guard let arguments = call.arguments as? [String: Any], 30 | let path = arguments["file"] as? String, 31 | let _ = arguments["name"], 32 | let isReturnFilePath = arguments["isReturnPathOfIOS"] as? Bool else { return } 33 | if (isImageFile(filename: path)) { 34 | saveImageAtFileUrl(path, isReturnImagePath: isReturnFilePath) 35 | } else { 36 | if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(path)) { 37 | saveVideo(path, isReturnImagePath: isReturnFilePath) 38 | }else{ 39 | self.saveResult(isSuccess:false,error:self.errorMessage) 40 | } 41 | } 42 | } else { 43 | result(FlutterMethodNotImplemented) 44 | } 45 | } 46 | 47 | func saveVideo(_ path: String, isReturnImagePath: Bool) { 48 | if !isReturnImagePath { 49 | UISaveVideoAtPathToSavedPhotosAlbum(path, self, #selector(didFinishSavingVideo(videoPath:error:contextInfo:)), nil) 50 | return 51 | } 52 | var videoIds: [String] = [] 53 | 54 | PHPhotoLibrary.shared().performChanges( { 55 | let req = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL.init(fileURLWithPath: path)) 56 | if let videoId = req?.placeholderForCreatedAsset?.localIdentifier { 57 | videoIds.append(videoId) 58 | } 59 | }, completionHandler: { [unowned self] (success, error) in 60 | DispatchQueue.main.async { 61 | if (success && videoIds.count > 0) { 62 | let assetResult = PHAsset.fetchAssets(withLocalIdentifiers: videoIds, options: nil) 63 | if (assetResult.count > 0) { 64 | let videoAsset = assetResult[0] 65 | PHImageManager().requestAVAsset(forVideo: videoAsset, options: nil) { (avurlAsset, audioMix, info) in 66 | if let urlStr = (avurlAsset as? AVURLAsset)?.url.absoluteString { 67 | self.saveResult(isSuccess: true, filePath: urlStr) 68 | } 69 | } 70 | } 71 | } else { 72 | self.saveResult(isSuccess: false, error: self.errorMessage) 73 | } 74 | } 75 | }) 76 | } 77 | 78 | func saveImage(_ image: UIImage, isReturnImagePath: Bool) { 79 | if !isReturnImagePath { 80 | UIImageWriteToSavedPhotosAlbum(image, self, #selector(didFinishSavingImage(image:error:contextInfo:)), nil) 81 | return 82 | } 83 | 84 | var imageIds: [String] = [] 85 | 86 | PHPhotoLibrary.shared().performChanges( { 87 | let req = PHAssetChangeRequest.creationRequestForAsset(from: image) 88 | if let imageId = req.placeholderForCreatedAsset?.localIdentifier { 89 | imageIds.append(imageId) 90 | } 91 | }, completionHandler: { [unowned self] (success, error) in 92 | DispatchQueue.main.async { 93 | if (success && imageIds.count > 0) { 94 | let assetResult = PHAsset.fetchAssets(withLocalIdentifiers: imageIds, options: nil) 95 | if (assetResult.count > 0) { 96 | let imageAsset = assetResult[0] 97 | let options = PHContentEditingInputRequestOptions() 98 | options.canHandleAdjustmentData = { (adjustmeta) 99 | -> Bool in true } 100 | imageAsset.requestContentEditingInput(with: options) { [unowned self] (contentEditingInput, info) in 101 | if let urlStr = contentEditingInput?.fullSizeImageURL?.absoluteString { 102 | self.saveResult(isSuccess: true, filePath: urlStr) 103 | } 104 | } 105 | } 106 | } else { 107 | self.saveResult(isSuccess: false, error: self.errorMessage) 108 | } 109 | } 110 | }) 111 | } 112 | 113 | func saveImageAtFileUrl(_ url: String, isReturnImagePath: Bool) { 114 | if !isReturnImagePath { 115 | if let image = UIImage(contentsOfFile: url) { 116 | UIImageWriteToSavedPhotosAlbum(image, self, #selector(didFinishSavingImage(image:error:contextInfo:)), nil) 117 | } 118 | return 119 | } 120 | 121 | var imageIds: [String] = [] 122 | 123 | PHPhotoLibrary.shared().performChanges( { 124 | let req = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: URL(string: url)!) 125 | if let imageId = req?.placeholderForCreatedAsset?.localIdentifier { 126 | imageIds.append(imageId) 127 | } 128 | }, completionHandler: { [unowned self] (success, error) in 129 | DispatchQueue.main.async { 130 | if (success && imageIds.count > 0) { 131 | let assetResult = PHAsset.fetchAssets(withLocalIdentifiers: imageIds, options: nil) 132 | if (assetResult.count > 0) { 133 | let imageAsset = assetResult[0] 134 | let options = PHContentEditingInputRequestOptions() 135 | options.canHandleAdjustmentData = { (adjustmeta) 136 | -> Bool in true } 137 | imageAsset.requestContentEditingInput(with: options) { [unowned self] (contentEditingInput, info) in 138 | if let urlStr = contentEditingInput?.fullSizeImageURL?.absoluteString { 139 | self.saveResult(isSuccess: true, filePath: urlStr) 140 | } 141 | } 142 | } 143 | } else { 144 | self.saveResult(isSuccess: false, error: self.errorMessage) 145 | } 146 | } 147 | }) 148 | } 149 | 150 | /// finish saving,if has error,parameters error will not nill 151 | @objc func didFinishSavingImage(image: UIImage, error: NSError?, contextInfo: UnsafeMutableRawPointer?) { 152 | saveResult(isSuccess: error == nil, error: error?.description) 153 | } 154 | 155 | @objc func didFinishSavingVideo(videoPath: String, error: NSError?, contextInfo: UnsafeMutableRawPointer?) { 156 | saveResult(isSuccess: error == nil, error: error?.description) 157 | } 158 | 159 | func saveResult(isSuccess: Bool, error: String? = nil, filePath: String? = nil) { 160 | var saveResult = SaveResultModel() 161 | saveResult.isSuccess = error == nil 162 | saveResult.errorMessage = error?.description 163 | saveResult.filePath = filePath 164 | result?(saveResult.toDic()) 165 | } 166 | 167 | func isImageFile(filename: String) -> Bool { 168 | return filename.hasSuffix(".jpg") 169 | || filename.hasSuffix(".png") 170 | || filename.hasSuffix(".jpeg") 171 | || filename.hasSuffix(".JPEG") 172 | || filename.hasSuffix(".JPG") 173 | || filename.hasSuffix(".PNG") 174 | || filename.hasSuffix(".gif") 175 | || filename.hasSuffix(".GIF") 176 | || filename.hasSuffix(".heic") 177 | || filename.hasSuffix(".HEIC") 178 | } 179 | } 180 | 181 | public struct SaveResultModel: Encodable { 182 | var isSuccess: Bool! 183 | var filePath: String? 184 | var errorMessage: String? 185 | 186 | func toDic() -> [String:Any]? { 187 | let encoder = JSONEncoder() 188 | guard let data = try? encoder.encode(self) else { return nil } 189 | if (!JSONSerialization.isValidJSONObject(data)) { 190 | return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any] 191 | } 192 | return nil 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/example/imagegallerysaver/ImageGallerySaverPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.example.imagegallerysaver 2 | 3 | import androidx.annotation.NonNull 4 | import android.annotation.TargetApi 5 | import android.content.ContentValues 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.graphics.Bitmap 9 | import android.graphics.BitmapFactory 10 | import android.media.MediaScannerConnection 11 | import android.net.Uri 12 | import android.os.Environment 13 | import android.os.Build 14 | import android.provider.MediaStore 15 | import io.flutter.embedding.engine.plugins.FlutterPlugin 16 | import io.flutter.plugin.common.BinaryMessenger 17 | import io.flutter.plugin.common.MethodCall 18 | import io.flutter.plugin.common.MethodChannel 19 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 20 | import io.flutter.plugin.common.MethodChannel.Result 21 | import io.flutter.plugin.common.PluginRegistry.Registrar 22 | import java.io.File 23 | import java.io.FileInputStream 24 | import java.io.IOException 25 | import android.text.TextUtils 26 | import android.webkit.MimeTypeMap 27 | import java.io.OutputStream 28 | 29 | class ImageGallerySaverPlugin : FlutterPlugin, MethodCallHandler { 30 | private lateinit var methodChannel: MethodChannel 31 | private var applicationContext: Context? = null 32 | 33 | override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 34 | this.applicationContext = binding.applicationContext 35 | methodChannel = MethodChannel(binding.binaryMessenger, "image_gallery_saver") 36 | methodChannel.setMethodCallHandler(this) 37 | } 38 | 39 | override fun onMethodCall(@NonNull call: MethodCall,@NonNull result: Result): Unit { 40 | when (call.method) { 41 | "saveImageToGallery" -> { 42 | val image = call.argument("imageBytes") 43 | val quality = call.argument("quality") 44 | val name = call.argument("name") 45 | 46 | result.success( 47 | saveImageToGallery( 48 | BitmapFactory.decodeByteArray( 49 | image ?: ByteArray(0), 50 | 0, 51 | image?.size ?: 0 52 | ), quality, name 53 | ) 54 | ) 55 | } 56 | 57 | "saveFileToGallery" -> { 58 | val path = call.argument("file") 59 | val name = call.argument("name") 60 | result.success(saveFileToGallery(path, name)) 61 | } 62 | 63 | else -> result.notImplemented() 64 | } 65 | } 66 | 67 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 68 | applicationContext = null 69 | methodChannel.setMethodCallHandler(null); 70 | } 71 | 72 | private fun generateUri(extension: String = "", name: String? = null): Uri? { 73 | var fileName = name ?: System.currentTimeMillis().toString() 74 | val mimeType = getMIMEType(extension) 75 | val isVideo = mimeType?.startsWith("video")==true 76 | 77 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 78 | // >= android 10 79 | val uri = when { 80 | isVideo -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI 81 | else -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI 82 | } 83 | 84 | val values = ContentValues().apply { 85 | put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) 86 | put( 87 | MediaStore.MediaColumns.RELATIVE_PATH, when { 88 | isVideo -> Environment.DIRECTORY_MOVIES 89 | else -> Environment.DIRECTORY_PICTURES 90 | } 91 | ) 92 | if (!TextUtils.isEmpty(mimeType)) { 93 | put(when {isVideo -> MediaStore.Video.Media.MIME_TYPE 94 | else -> MediaStore.Images.Media.MIME_TYPE 95 | }, mimeType) 96 | } 97 | } 98 | 99 | applicationContext?.contentResolver?.insert(uri, values) 100 | 101 | } else { 102 | // < android 10 103 | val storePath = 104 | Environment.getExternalStoragePublicDirectory(when { 105 | isVideo -> Environment.DIRECTORY_MOVIES 106 | else -> Environment.DIRECTORY_PICTURES 107 | }).absolutePath 108 | val appDir = File(storePath).apply { 109 | if (!exists()) { 110 | mkdir() 111 | } 112 | } 113 | 114 | val file = 115 | File(appDir, if (extension.isNotEmpty()) "$fileName.$extension" else fileName) 116 | Uri.fromFile(file) 117 | } 118 | } 119 | 120 | /** 121 | * get file Mime Type 122 | * 123 | * @param extension extension 124 | * @return file Mime Type 125 | */ 126 | private fun getMIMEType(extension: String): String? { 127 | return if (!TextUtils.isEmpty(extension)) { 128 | MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.lowercase()) 129 | } else { 130 | null 131 | } 132 | } 133 | 134 | /** 135 | * Send storage success notification 136 | * 137 | * @param context context 138 | * @param fileUri file path 139 | */ 140 | private fun sendBroadcast(context: Context, fileUri: Uri?) { 141 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 142 | val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) 143 | mediaScanIntent.data = fileUri 144 | context.sendBroadcast(mediaScanIntent) 145 | } 146 | } 147 | 148 | private fun saveImageToGallery( 149 | bmp: Bitmap?, 150 | quality: Int?, 151 | name: String? 152 | ): HashMap { 153 | // check parameters 154 | if (bmp == null || quality == null) { 155 | return SaveResultModel(false, null, "parameters error").toHashMap() 156 | } 157 | // check applicationContext 158 | val context = applicationContext 159 | ?: return SaveResultModel(false, null, "applicationContext null").toHashMap() 160 | var fileUri: Uri? = null 161 | var fos: OutputStream? = null 162 | var success = false 163 | try { 164 | fileUri = generateUri("jpg", name = name) 165 | if (fileUri != null) { 166 | fos = context.contentResolver.openOutputStream(fileUri) 167 | if (fos != null) { 168 | println("ImageGallerySaverPlugin $quality") 169 | bmp.compress(Bitmap.CompressFormat.JPEG, quality, fos) 170 | fos.flush() 171 | success = true 172 | } 173 | } 174 | } catch (e: IOException) { 175 | SaveResultModel(false, null, e.toString()).toHashMap() 176 | } finally { 177 | fos?.close() 178 | bmp.recycle() 179 | } 180 | return if (success) { 181 | sendBroadcast(context, fileUri) 182 | SaveResultModel(fileUri.toString().isNotEmpty(), fileUri.toString(), null).toHashMap() 183 | } else { 184 | SaveResultModel(false, null, "saveImageToGallery fail").toHashMap() 185 | } 186 | } 187 | 188 | private fun saveFileToGallery(filePath: String?, name: String?): HashMap { 189 | // check parameters 190 | if (filePath == null) { 191 | return SaveResultModel(false, null, "parameters error").toHashMap() 192 | } 193 | val context = applicationContext ?: return SaveResultModel( 194 | false, 195 | null, 196 | "applicationContext null" 197 | ).toHashMap() 198 | var fileUri: Uri? = null 199 | var outputStream: OutputStream? = null 200 | var fileInputStream: FileInputStream? = null 201 | var success = false 202 | 203 | try { 204 | val originalFile = File(filePath) 205 | if(!originalFile.exists()) return SaveResultModel(false, null, "$filePath does not exist").toHashMap() 206 | fileUri = generateUri(originalFile.extension, name) 207 | if (fileUri != null) { 208 | outputStream = context.contentResolver?.openOutputStream(fileUri) 209 | if (outputStream != null) { 210 | fileInputStream = FileInputStream(originalFile) 211 | 212 | val buffer = ByteArray(10240) 213 | var count = 0 214 | while (fileInputStream.read(buffer).also { count = it } > 0) { 215 | outputStream.write(buffer, 0, count) 216 | } 217 | 218 | outputStream.flush() 219 | success = true 220 | } 221 | } 222 | } catch (e: IOException) { 223 | SaveResultModel(false, null, e.toString()).toHashMap() 224 | } finally { 225 | outputStream?.close() 226 | fileInputStream?.close() 227 | } 228 | return if (success) { 229 | sendBroadcast(context, fileUri) 230 | SaveResultModel(fileUri.toString().isNotEmpty(), fileUri.toString(), null).toHashMap() 231 | } else { 232 | SaveResultModel(false, null, "saveFileToGallery fail").toHashMap() 233 | } 234 | } 235 | } 236 | 237 | class SaveResultModel(var isSuccess: Boolean, 238 | var filePath: String? = null, 239 | var errorMessage: String? = null) { 240 | fun toHashMap(): HashMap { 241 | val hashMap = HashMap() 242 | hashMap["isSuccess"] = isSuccess 243 | hashMap["filePath"] = filePath 244 | hashMap["errorMessage"] = errorMessage 245 | return hashMap 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 4665DAD90E8BD38BFB8111A4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11CB91839384355D96BBB58C /* Pods_Runner.framework */; }; 13 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 14 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 15 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 16 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 026117FF47F6FEB1E07D9481 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 34 | 11CB91839384355D96BBB58C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 36 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 37 | 1B25BBAEEA2C12CCC61636D4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 38 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 39 | 4F4E4CE4EAC8F5376068C322 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 40 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 41 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 43 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 44 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 45 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | 4665DAD90E8BD38BFB8111A4 /* Pods_Runner.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 9740EEB11CF90186004384FC /* Flutter */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 68 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 69 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 70 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 71 | ); 72 | name = Flutter; 73 | sourceTree = ""; 74 | }; 75 | 977B1D48C0E48E96D51532D2 /* Frameworks */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 11CB91839384355D96BBB58C /* Pods_Runner.framework */, 79 | ); 80 | name = Frameworks; 81 | sourceTree = ""; 82 | }; 83 | 97C146E51CF9000F007C117D = { 84 | isa = PBXGroup; 85 | children = ( 86 | 9740EEB11CF90186004384FC /* Flutter */, 87 | 97C146F01CF9000F007C117D /* Runner */, 88 | 97C146EF1CF9000F007C117D /* Products */, 89 | B68C3009456C69A3E0E737B9 /* Pods */, 90 | 977B1D48C0E48E96D51532D2 /* Frameworks */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | 97C146EF1CF9000F007C117D /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 97C146EE1CF9000F007C117D /* Runner.app */, 98 | ); 99 | name = Products; 100 | sourceTree = ""; 101 | }; 102 | 97C146F01CF9000F007C117D /* Runner */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 106 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 107 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 108 | 97C147021CF9000F007C117D /* Info.plist */, 109 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 110 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 111 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 112 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 113 | ); 114 | path = Runner; 115 | sourceTree = ""; 116 | }; 117 | B68C3009456C69A3E0E737B9 /* Pods */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 026117FF47F6FEB1E07D9481 /* Pods-Runner.debug.xcconfig */, 121 | 4F4E4CE4EAC8F5376068C322 /* Pods-Runner.release.xcconfig */, 122 | 1B25BBAEEA2C12CCC61636D4 /* Pods-Runner.profile.xcconfig */, 123 | ); 124 | path = Pods; 125 | sourceTree = ""; 126 | }; 127 | /* End PBXGroup section */ 128 | 129 | /* Begin PBXNativeTarget section */ 130 | 97C146ED1CF9000F007C117D /* Runner */ = { 131 | isa = PBXNativeTarget; 132 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 133 | buildPhases = ( 134 | 1EDE0E94DBE71CD083227E90 /* [CP] Check Pods Manifest.lock */, 135 | 9740EEB61CF901F6004384FC /* Run Script */, 136 | 97C146EA1CF9000F007C117D /* Sources */, 137 | 97C146EB1CF9000F007C117D /* Frameworks */, 138 | 97C146EC1CF9000F007C117D /* Resources */, 139 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 140 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 141 | 74E1176F028703D33774BD4F /* [CP] Embed Pods Frameworks */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = Runner; 148 | productName = Runner; 149 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | /* End PBXNativeTarget section */ 153 | 154 | /* Begin PBXProject section */ 155 | 97C146E61CF9000F007C117D /* Project object */ = { 156 | isa = PBXProject; 157 | attributes = { 158 | LastUpgradeCheck = 1300; 159 | ORGANIZATIONNAME = ""; 160 | TargetAttributes = { 161 | 97C146ED1CF9000F007C117D = { 162 | CreatedOnToolsVersion = 7.3.1; 163 | LastSwiftMigration = 1100; 164 | }; 165 | }; 166 | }; 167 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 168 | compatibilityVersion = "Xcode 9.3"; 169 | developmentRegion = en; 170 | hasScannedForEncodings = 0; 171 | knownRegions = ( 172 | en, 173 | Base, 174 | ); 175 | mainGroup = 97C146E51CF9000F007C117D; 176 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 177 | projectDirPath = ""; 178 | projectRoot = ""; 179 | targets = ( 180 | 97C146ED1CF9000F007C117D /* Runner */, 181 | ); 182 | }; 183 | /* End PBXProject section */ 184 | 185 | /* Begin PBXResourcesBuildPhase section */ 186 | 97C146EC1CF9000F007C117D /* Resources */ = { 187 | isa = PBXResourcesBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 191 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 192 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 193 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | /* End PBXResourcesBuildPhase section */ 198 | 199 | /* Begin PBXShellScriptBuildPhase section */ 200 | 1EDE0E94DBE71CD083227E90 /* [CP] Check Pods Manifest.lock */ = { 201 | isa = PBXShellScriptBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | ); 205 | inputFileListPaths = ( 206 | ); 207 | inputPaths = ( 208 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 209 | "${PODS_ROOT}/Manifest.lock", 210 | ); 211 | name = "[CP] Check Pods Manifest.lock"; 212 | outputFileListPaths = ( 213 | ); 214 | outputPaths = ( 215 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | shellPath = /bin/sh; 219 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 220 | showEnvVarsInLog = 0; 221 | }; 222 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 223 | isa = PBXShellScriptBuildPhase; 224 | alwaysOutOfDate = 1; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | ); 228 | inputPaths = ( 229 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 230 | ); 231 | name = "Thin Binary"; 232 | outputPaths = ( 233 | ); 234 | runOnlyForDeploymentPostprocessing = 0; 235 | shellPath = /bin/sh; 236 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 237 | }; 238 | 74E1176F028703D33774BD4F /* [CP] Embed Pods Frameworks */ = { 239 | isa = PBXShellScriptBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | ); 243 | inputFileListPaths = ( 244 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 245 | ); 246 | name = "[CP] Embed Pods Frameworks"; 247 | outputFileListPaths = ( 248 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | shellPath = /bin/sh; 252 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 253 | showEnvVarsInLog = 0; 254 | }; 255 | 9740EEB61CF901F6004384FC /* Run Script */ = { 256 | isa = PBXShellScriptBuildPhase; 257 | alwaysOutOfDate = 1; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | ); 261 | inputPaths = ( 262 | ); 263 | name = "Run Script"; 264 | outputPaths = ( 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | shellPath = /bin/sh; 268 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 269 | }; 270 | /* End PBXShellScriptBuildPhase section */ 271 | 272 | /* Begin PBXSourcesBuildPhase section */ 273 | 97C146EA1CF9000F007C117D /* Sources */ = { 274 | isa = PBXSourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 278 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | }; 282 | /* End PBXSourcesBuildPhase section */ 283 | 284 | /* Begin PBXVariantGroup section */ 285 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 286 | isa = PBXVariantGroup; 287 | children = ( 288 | 97C146FB1CF9000F007C117D /* Base */, 289 | ); 290 | name = Main.storyboard; 291 | sourceTree = ""; 292 | }; 293 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 294 | isa = PBXVariantGroup; 295 | children = ( 296 | 97C147001CF9000F007C117D /* Base */, 297 | ); 298 | name = LaunchScreen.storyboard; 299 | sourceTree = ""; 300 | }; 301 | /* End PBXVariantGroup section */ 302 | 303 | /* Begin XCBuildConfiguration section */ 304 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ALWAYS_SEARCH_USER_PATHS = NO; 308 | CLANG_ANALYZER_NONNULL = YES; 309 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 310 | CLANG_CXX_LIBRARY = "libc++"; 311 | CLANG_ENABLE_MODULES = YES; 312 | CLANG_ENABLE_OBJC_ARC = YES; 313 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 314 | CLANG_WARN_BOOL_CONVERSION = YES; 315 | CLANG_WARN_COMMA = YES; 316 | CLANG_WARN_CONSTANT_CONVERSION = YES; 317 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 318 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 319 | CLANG_WARN_EMPTY_BODY = YES; 320 | CLANG_WARN_ENUM_CONVERSION = YES; 321 | CLANG_WARN_INFINITE_RECURSION = YES; 322 | CLANG_WARN_INT_CONVERSION = YES; 323 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 325 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 326 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 327 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 328 | CLANG_WARN_STRICT_PROTOTYPES = YES; 329 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 330 | CLANG_WARN_UNREACHABLE_CODE = YES; 331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 332 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 333 | COPY_PHASE_STRIP = NO; 334 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 335 | ENABLE_NS_ASSERTIONS = NO; 336 | ENABLE_STRICT_OBJC_MSGSEND = YES; 337 | GCC_C_LANGUAGE_STANDARD = gnu99; 338 | GCC_NO_COMMON_BLOCKS = YES; 339 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 340 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 341 | GCC_WARN_UNDECLARED_SELECTOR = YES; 342 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 343 | GCC_WARN_UNUSED_FUNCTION = YES; 344 | GCC_WARN_UNUSED_VARIABLE = YES; 345 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 346 | MTL_ENABLE_DEBUG_INFO = NO; 347 | SDKROOT = iphoneos; 348 | SUPPORTED_PLATFORMS = iphoneos; 349 | TARGETED_DEVICE_FAMILY = "1,2"; 350 | VALIDATE_PRODUCT = YES; 351 | }; 352 | name = Profile; 353 | }; 354 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 355 | isa = XCBuildConfiguration; 356 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 357 | buildSettings = { 358 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 359 | CLANG_ENABLE_MODULES = YES; 360 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 361 | DEVELOPMENT_TEAM = V92CX3HWGS; 362 | ENABLE_BITCODE = NO; 363 | INFOPLIST_FILE = Runner/Info.plist; 364 | LD_RUNPATH_SEARCH_PATHS = ( 365 | "$(inherited)", 366 | "@executable_path/Frameworks", 367 | ); 368 | PRODUCT_BUNDLE_IDENTIFIER = com.example.imageGallerySaverExample; 369 | PRODUCT_NAME = "$(TARGET_NAME)"; 370 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 371 | SWIFT_VERSION = 5.0; 372 | VERSIONING_SYSTEM = "apple-generic"; 373 | }; 374 | name = Profile; 375 | }; 376 | 97C147031CF9000F007C117D /* Debug */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_ANALYZER_NONNULL = YES; 381 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 382 | CLANG_CXX_LIBRARY = "libc++"; 383 | CLANG_ENABLE_MODULES = YES; 384 | CLANG_ENABLE_OBJC_ARC = YES; 385 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 386 | CLANG_WARN_BOOL_CONVERSION = YES; 387 | CLANG_WARN_COMMA = YES; 388 | CLANG_WARN_CONSTANT_CONVERSION = YES; 389 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 390 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 400 | CLANG_WARN_STRICT_PROTOTYPES = YES; 401 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 402 | CLANG_WARN_UNREACHABLE_CODE = YES; 403 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 404 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 405 | COPY_PHASE_STRIP = NO; 406 | DEBUG_INFORMATION_FORMAT = dwarf; 407 | ENABLE_STRICT_OBJC_MSGSEND = YES; 408 | ENABLE_TESTABILITY = YES; 409 | GCC_C_LANGUAGE_STANDARD = gnu99; 410 | GCC_DYNAMIC_NO_PIC = NO; 411 | GCC_NO_COMMON_BLOCKS = YES; 412 | GCC_OPTIMIZATION_LEVEL = 0; 413 | GCC_PREPROCESSOR_DEFINITIONS = ( 414 | "DEBUG=1", 415 | "$(inherited)", 416 | ); 417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 419 | GCC_WARN_UNDECLARED_SELECTOR = YES; 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 421 | GCC_WARN_UNUSED_FUNCTION = YES; 422 | GCC_WARN_UNUSED_VARIABLE = YES; 423 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 424 | MTL_ENABLE_DEBUG_INFO = YES; 425 | ONLY_ACTIVE_ARCH = YES; 426 | SDKROOT = iphoneos; 427 | TARGETED_DEVICE_FAMILY = "1,2"; 428 | }; 429 | name = Debug; 430 | }; 431 | 97C147041CF9000F007C117D /* Release */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | ALWAYS_SEARCH_USER_PATHS = NO; 435 | CLANG_ANALYZER_NONNULL = YES; 436 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 437 | CLANG_CXX_LIBRARY = "libc++"; 438 | CLANG_ENABLE_MODULES = YES; 439 | CLANG_ENABLE_OBJC_ARC = YES; 440 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 441 | CLANG_WARN_BOOL_CONVERSION = YES; 442 | CLANG_WARN_COMMA = YES; 443 | CLANG_WARN_CONSTANT_CONVERSION = YES; 444 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 445 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 446 | CLANG_WARN_EMPTY_BODY = YES; 447 | CLANG_WARN_ENUM_CONVERSION = YES; 448 | CLANG_WARN_INFINITE_RECURSION = YES; 449 | CLANG_WARN_INT_CONVERSION = YES; 450 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 451 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 452 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 453 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 455 | CLANG_WARN_STRICT_PROTOTYPES = YES; 456 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 457 | CLANG_WARN_UNREACHABLE_CODE = YES; 458 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 459 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 460 | COPY_PHASE_STRIP = NO; 461 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 462 | ENABLE_NS_ASSERTIONS = NO; 463 | ENABLE_STRICT_OBJC_MSGSEND = YES; 464 | GCC_C_LANGUAGE_STANDARD = gnu99; 465 | GCC_NO_COMMON_BLOCKS = YES; 466 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 467 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 468 | GCC_WARN_UNDECLARED_SELECTOR = YES; 469 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 470 | GCC_WARN_UNUSED_FUNCTION = YES; 471 | GCC_WARN_UNUSED_VARIABLE = YES; 472 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 473 | MTL_ENABLE_DEBUG_INFO = NO; 474 | SDKROOT = iphoneos; 475 | SUPPORTED_PLATFORMS = iphoneos; 476 | SWIFT_COMPILATION_MODE = wholemodule; 477 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 478 | TARGETED_DEVICE_FAMILY = "1,2"; 479 | VALIDATE_PRODUCT = YES; 480 | }; 481 | name = Release; 482 | }; 483 | 97C147061CF9000F007C117D /* Debug */ = { 484 | isa = XCBuildConfiguration; 485 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 486 | buildSettings = { 487 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 488 | CLANG_ENABLE_MODULES = YES; 489 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 490 | DEVELOPMENT_TEAM = V92CX3HWGS; 491 | ENABLE_BITCODE = NO; 492 | INFOPLIST_FILE = Runner/Info.plist; 493 | LD_RUNPATH_SEARCH_PATHS = ( 494 | "$(inherited)", 495 | "@executable_path/Frameworks", 496 | ); 497 | PRODUCT_BUNDLE_IDENTIFIER = com.example.imageGallerySaverExample; 498 | PRODUCT_NAME = "$(TARGET_NAME)"; 499 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 500 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 501 | SWIFT_VERSION = 5.0; 502 | VERSIONING_SYSTEM = "apple-generic"; 503 | }; 504 | name = Debug; 505 | }; 506 | 97C147071CF9000F007C117D /* Release */ = { 507 | isa = XCBuildConfiguration; 508 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 509 | buildSettings = { 510 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 511 | CLANG_ENABLE_MODULES = YES; 512 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 513 | DEVELOPMENT_TEAM = V92CX3HWGS; 514 | ENABLE_BITCODE = NO; 515 | INFOPLIST_FILE = Runner/Info.plist; 516 | LD_RUNPATH_SEARCH_PATHS = ( 517 | "$(inherited)", 518 | "@executable_path/Frameworks", 519 | ); 520 | PRODUCT_BUNDLE_IDENTIFIER = com.example.imageGallerySaverExample; 521 | PRODUCT_NAME = "$(TARGET_NAME)"; 522 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 523 | SWIFT_VERSION = 5.0; 524 | VERSIONING_SYSTEM = "apple-generic"; 525 | }; 526 | name = Release; 527 | }; 528 | /* End XCBuildConfiguration section */ 529 | 530 | /* Begin XCConfigurationList section */ 531 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 532 | isa = XCConfigurationList; 533 | buildConfigurations = ( 534 | 97C147031CF9000F007C117D /* Debug */, 535 | 97C147041CF9000F007C117D /* Release */, 536 | 249021D3217E4FDB00AE95B9 /* Profile */, 537 | ); 538 | defaultConfigurationIsVisible = 0; 539 | defaultConfigurationName = Release; 540 | }; 541 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 542 | isa = XCConfigurationList; 543 | buildConfigurations = ( 544 | 97C147061CF9000F007C117D /* Debug */, 545 | 97C147071CF9000F007C117D /* Release */, 546 | 249021D4217E4FDB00AE95B9 /* Profile */, 547 | ); 548 | defaultConfigurationIsVisible = 0; 549 | defaultConfigurationName = Release; 550 | }; 551 | /* End XCConfigurationList section */ 552 | }; 553 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 554 | } 555 | --------------------------------------------------------------------------------