commit c8367c3454a603f6065c314b976d0924beeff5a0 Author: ayub Date: Sun Aug 10 13:32:44 2025 +0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b365c08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ +.idea/ +.fvm/ + +pubspec.lock + +build/ + +*.iml \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..51497f3 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 6a3ff018b199a7febbe2b5adbb564081d8f49e2f + channel: dev + +project_type: plugin diff --git a/.travis.yaml b/.travis.yaml new file mode 100644 index 0000000..fc0d22e --- /dev/null +++ b/.travis.yaml @@ -0,0 +1,19 @@ +os: + - linux +sudo: false +addons: + apt: + # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 + sources: + - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version + packages: + - libstdc++6 + - fonts-droid-fallback +before_script: + - git clone https://github.com/flutter/flutter.git -b stable --depth 1 + - ./flutter/bin/flutter doctor +script: + - ./flutter/bin/flutter test +cache: + directories: + - $HOME/.pub-cache diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fc0d22e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +os: + - linux +sudo: false +addons: + apt: + # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 + sources: + - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version + packages: + - libstdc++6 + - fonts-droid-fallback +before_script: + - git clone https://github.com/flutter/flutter.git -b stable --depth 1 + - ./flutter/bin/flutter doctor +script: + - ./flutter/bin/flutter test +cache: + directories: + - $HOME/.pub-cache diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aa84ee0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,106 @@ +## 2.0.3 +- 1.Upgrade flutter version to 3.10.5 +- 2.Android build tools are upgraded to 7.3.0 +- 3.Optimize the Android plugin library code + +## 2.0.2 +- 1.Optimization android plugin + +## 2.0.1 +- 1.Upgrade flutter version to 3.10.2 +- 2.Upgrade Android/ios plug-in related +- 3.Support Android 13 +- 4.Support ios16 + +## 1.7.1 +- optimization + +## 1.7.0 +- optimization + +## 1.6.9 +- optimization + +## 1.6.8 +- Support android 11 save + +## 1.6.7 +- fix ios bug + +## 1.6.6 +* fix ios bug + +## 1.6.5 +* fix android bug + +## 1.6.4 +* formatted code + +## 1.6.3 +* Save result return more message + +## 1.6.2 +* fix crash on iOS when granting permission + +## 1.6.1 +* fix iOS Swift5.1 error + +## 1.6.0 +* Support iOS return save path + +## 1.5.0 +* Save image with JPG and Support special quality(ios & Android) +* Support special Image name for Android +* Upgrade libraries and dependence +* fix docs +* Add more example + +## 1.3.0 + +* Define clang module for static ios builds +* Cleanup example project + +## 1.2.2 + +* Migrate to AndroidX +* optimize git ignore + +## 1.2.1 + +* Support return path for Android. +* Fix bug(save video fail for Android). + +## 1.2.0 + +* Support video save and file path to gallery +* Add example for save video and net image + +## 1.1.0 + +* Upgrade kotlin(1.3.20) and gradle build plugin version(3.3.0). + +## 1.0.0 + +* Updated Kotlin Gradle plugin version + +## 0.1.2 + +* Remove hard coded path - image_gallery_saver in Android + +## 0.1.1 + +* Updated README and Description + +## 0.1.0 + +* Updated README and CHANGELOG +* Add LICENSE +* Add Test + +## 0.0.2 + +* Updated README and CHANGELOG + +## 0.0.1 + +* Initial Open Source release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c72f3cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2023] [zaihui] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..49bd2a7 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# image_gallery_saver + +[![Build Status](https://travis-ci.org/hui-z/image_gallery_saver.svg?branch=master)](https://travis-ci.org/hui-z/image_gallery_saver#) +[![pub package](https://img.shields.io/pub/v/image_gallery_saver.svg)](https://pub.dartlang.org/packages/image_gallery_saver) +[![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://choosealicense.com/licenses/mit/) + +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. + +## Usage + +To use this plugin, add `image_gallery_saver` as a dependency in your pubspec.yaml file. For example: +```yaml +dependencies: + image_gallery_saver: '^2.0.3' +``` + +## iOS +Your project need create with swift. +Add the following keys to your Info.plist file, located in /ios/Runner/Info.plist: + * NSPhotoLibraryAddUsageDescription - describe why your app needs permission for the photo library. This is called Privacy - Photo Library Additions Usage Description in the visual editor + + ## Android + 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). + In Android version 10, Open the manifest file and add this line to your application tag + ``` + + ``` + +## Example +Saving an image from the internet, quality and name is option +``` dart + _saveLocalImage() async { + RenderRepaintBoundary boundary = + _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; + ui.Image image = await boundary.toImage(); + ByteData? byteData = + await (image.toByteData(format: ui.ImageByteFormat.png)); + if (byteData != null) { + final result = + await ImageGallerySaver.saveImage(byteData.buffer.asUint8List()); + print(result); + } + } + + _saveNetworkImage() async { + var response = await Dio().get( + "https://ss0.baidu.com/94o3dSag_xI4khGko9WTAnF6hhy/image/h%3D300/sign=a62e824376d98d1069d40a31113eb807/838ba61ea8d3fd1fc9c7b6853a4e251f94ca5f46.jpg", + options: Options(responseType: ResponseType.bytes)); + final result = await ImageGallerySaver.saveImage( + Uint8List.fromList(response.data), + quality: 60, + name: "hello"); + print(result); + } +``` + +Saving file(ig: video/gif/others) from the internet +``` dart + _saveNetworkGifFile() async { + var appDocDir = await getTemporaryDirectory(); + String savePath = appDocDir.path + "/temp.gif"; + String fileUrl = + "https://hyjdoc.oss-cn-beijing.aliyuncs.com/hyj-doc-flutter-demo-run.gif"; + await Dio().download(fileUrl, savePath); + final result = + await ImageGallerySaver.saveFile(savePath, isReturnPathOfIOS: true); + print(result); + } + + _saveNetworkVideoFile() async { + var appDocDir = await getTemporaryDirectory(); + String savePath = appDocDir.path + "/temp.mp4"; + String fileUrl = + "https://s3.cn-north-1.amazonaws.com.cn/mtab.kezaihui.com/video/ForBiggerBlazes.mp4"; + await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) { + print((count / total * 100).toStringAsFixed(0) + "%"); + }); + final result = await ImageGallerySaver.saveFile(savePath); + print(result); + } +``` diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..7ec7a09 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,45 @@ +group 'com.example.imagegallerysaver' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace 'com.example.imagegallerysaver' + compileSdkVersion 30 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..53ae0ae --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536M diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cf40b29 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Sep 09 11:20:21 CST 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..632310d --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'image_gallery_saver' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5f1f813 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/kotlin/com/example/imagegallerysaver/ImageGallerySaverPlugin.kt b/android/src/main/kotlin/com/example/imagegallerysaver/ImageGallerySaverPlugin.kt new file mode 100644 index 0000000..c88c80f --- /dev/null +++ b/android/src/main/kotlin/com/example/imagegallerysaver/ImageGallerySaverPlugin.kt @@ -0,0 +1,247 @@ +package com.example.imagegallerysaver + +import androidx.annotation.NonNull +import android.annotation.TargetApi +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Environment +import android.os.Build +import android.provider.MediaStore +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry.Registrar +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import android.text.TextUtils +import android.webkit.MimeTypeMap +import java.io.OutputStream + +class ImageGallerySaverPlugin : FlutterPlugin, MethodCallHandler { + private lateinit var methodChannel: MethodChannel + private var applicationContext: Context? = null + + override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + this.applicationContext = binding.applicationContext + methodChannel = MethodChannel(binding.binaryMessenger, "image_gallery_saver") + methodChannel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall,@NonNull result: Result): Unit { + when (call.method) { + "saveImageToGallery" -> { + val image = call.argument("imageBytes") + val quality = call.argument("quality") + val name = call.argument("name") + + result.success( + saveImageToGallery( + BitmapFactory.decodeByteArray( + image ?: ByteArray(0), + 0, + image?.size ?: 0 + ), quality, name + ) + ) + } + + "saveFileToGallery" -> { + val path = call.argument("file") + val name = call.argument("name") + result.success(saveFileToGallery(path, name)) + } + + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + applicationContext = null + methodChannel.setMethodCallHandler(null); + } + + private fun generateUri(extension: String = "", name: String? = null): Uri? { + var fileName = name ?: System.currentTimeMillis().toString() + val mimeType = getMIMEType(extension) + val isVideo = mimeType?.startsWith("video")==true + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // >= android 10 + val uri = when { + isVideo -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + else -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + + val values = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put( + MediaStore.MediaColumns.RELATIVE_PATH, when { + isVideo -> Environment.DIRECTORY_MOVIES + else -> Environment.DIRECTORY_PICTURES + } + ) + if (!TextUtils.isEmpty(mimeType)) { + put(when {isVideo -> MediaStore.Video.Media.MIME_TYPE + else -> MediaStore.Images.Media.MIME_TYPE + }, mimeType) + } + } + + applicationContext?.contentResolver?.insert(uri, values) + + } else { + // < android 10 + val storePath = + Environment.getExternalStoragePublicDirectory(when { + isVideo -> Environment.DIRECTORY_MOVIES + else -> Environment.DIRECTORY_PICTURES + }).absolutePath + val appDir = File(storePath).apply { + if (!exists()) { + mkdir() + } + } + + val file = + File(appDir, if (extension.isNotEmpty()) "$fileName.$extension" else fileName) + Uri.fromFile(file) + } + } + + /** + * get file Mime Type + * + * @param extension extension + * @return file Mime Type + */ + private fun getMIMEType(extension: String): String? { + return if (!TextUtils.isEmpty(extension)) { + MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.lowercase()) + } else { + null + } + } + + /** + * Send storage success notification + * + * @param context context + * @param fileUri file path + */ + private fun sendBroadcast(context: Context, fileUri: Uri?) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) + mediaScanIntent.data = fileUri + context.sendBroadcast(mediaScanIntent) + } + } + + private fun saveImageToGallery( + bmp: Bitmap?, + quality: Int?, + name: String? + ): HashMap { + // check parameters + if (bmp == null || quality == null) { + return SaveResultModel(false, null, "parameters error").toHashMap() + } + // check applicationContext + val context = applicationContext + ?: return SaveResultModel(false, null, "applicationContext null").toHashMap() + var fileUri: Uri? = null + var fos: OutputStream? = null + var success = false + try { + fileUri = generateUri("jpg", name = name) + if (fileUri != null) { + fos = context.contentResolver.openOutputStream(fileUri) + if (fos != null) { + println("ImageGallerySaverPlugin $quality") + bmp.compress(Bitmap.CompressFormat.JPEG, quality, fos) + fos.flush() + success = true + } + } + } catch (e: IOException) { + SaveResultModel(false, null, e.toString()).toHashMap() + } finally { + fos?.close() + bmp.recycle() + } + return if (success) { + sendBroadcast(context, fileUri) + SaveResultModel(fileUri.toString().isNotEmpty(), fileUri.toString(), null).toHashMap() + } else { + SaveResultModel(false, null, "saveImageToGallery fail").toHashMap() + } + } + + private fun saveFileToGallery(filePath: String?, name: String?): HashMap { + // check parameters + if (filePath == null) { + return SaveResultModel(false, null, "parameters error").toHashMap() + } + val context = applicationContext ?: return SaveResultModel( + false, + null, + "applicationContext null" + ).toHashMap() + var fileUri: Uri? = null + var outputStream: OutputStream? = null + var fileInputStream: FileInputStream? = null + var success = false + + try { + val originalFile = File(filePath) + if(!originalFile.exists()) return SaveResultModel(false, null, "$filePath does not exist").toHashMap() + fileUri = generateUri(originalFile.extension, name) + if (fileUri != null) { + outputStream = context.contentResolver?.openOutputStream(fileUri) + if (outputStream != null) { + fileInputStream = FileInputStream(originalFile) + + val buffer = ByteArray(10240) + var count = 0 + while (fileInputStream.read(buffer).also { count = it } > 0) { + outputStream.write(buffer, 0, count) + } + + outputStream.flush() + success = true + } + } + } catch (e: IOException) { + SaveResultModel(false, null, e.toString()).toHashMap() + } finally { + outputStream?.close() + fileInputStream?.close() + } + return if (success) { + sendBroadcast(context, fileUri) + SaveResultModel(fileUri.toString().isNotEmpty(), fileUri.toString(), null).toHashMap() + } else { + SaveResultModel(false, null, "saveFileToGallery fail").toHashMap() + } + } +} + +class SaveResultModel(var isSuccess: Boolean, + var filePath: String? = null, + var errorMessage: String? = null) { + fun toHashMap(): HashMap { + val hashMap = HashMap() + hashMap["isSuccess"] = isSuccess + hashMap["filePath"] = filePath + hashMap["errorMessage"] = errorMessage + return hashMap + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..879f404 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,73 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.flutter-plugins-dependencies + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..a59942b --- /dev/null +++ b/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 6a3ff018b199a7febbe2b5adbb564081d8f49e2f + channel: dev + +project_type: app diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..2bcb3f8 --- /dev/null +++ b/example/README.md @@ -0,0 +1,54 @@ +example/lib/main.dart + +```dart + _saveLocalImage() async { + RenderRepaintBoundary boundary = + _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; + ui.Image image = await boundary.toImage(); + ByteData? byteData = + await (image.toByteData(format: ui.ImageByteFormat.png)); + if (byteData != null) { + final result = + await ImageGallerySaver.saveImage(byteData.buffer.asUint8List()); + print(result); + Utils.toast(result.toString()); + } +} + +_saveNetworkImage() async { + var response = await Dio().get( + "https://ss0.baidu.com/94o3dSag_xI4khGko9WTAnF6hhy/image/h%3D300/sign=a62e824376d98d1069d40a31113eb807/838ba61ea8d3fd1fc9c7b6853a4e251f94ca5f46.jpg", + options: Options(responseType: ResponseType.bytes)); + final result = await ImageGallerySaver.saveImage( + Uint8List.fromList(response.data), + quality: 60, + name: "hello"); + print(result); + Utils.toast("$result"); +} + +_saveNetworkGifFile() async { + var appDocDir = await getTemporaryDirectory(); + String savePath = appDocDir.path + "/temp.gif"; + String fileUrl = + "https://hyjdoc.oss-cn-beijing.aliyuncs.com/hyj-doc-flutter-demo-run.gif"; + await Dio().download(fileUrl, savePath); + final result = + await ImageGallerySaver.saveFile(savePath, isReturnPathOfIOS: true); + print(result); + Utils.toast("$result"); +} + +_saveNetworkVideoFile() async { + var appDocDir = await getTemporaryDirectory(); + String savePath = appDocDir.path + "/temp.mp4"; + String fileUrl = + "https://s3.cn-north-1.amazonaws.com.cn/mtab.kezaihui.com/video/ForBiggerBlazes.mp4"; + await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) { + print((count / total * 100).toStringAsFixed(0) + "%"); + }); + final result = await ImageGallerySaver.saveFile(savePath); + print(result); + Utils.toast("$result"); +} +``` diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..42e7cab --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,76 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "com.example.imagegallerysaverexample" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..392ce3b --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..f7eb7f6 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..b6e61b6 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,4 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..352ad5f --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Sep 10 12:21:25 CST 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..f860b76 --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" \ No newline at end of file diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..50bb0cf --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# source '源地址' +source 'https://github.com/CocoaPods/Specs.git' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9e53bc2 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,554 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4665DAD90E8BD38BFB8111A4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11CB91839384355D96BBB58C /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 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 = ""; }; + 11CB91839384355D96BBB58C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 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 = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4665DAD90E8BD38BFB8111A4 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 977B1D48C0E48E96D51532D2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 11CB91839384355D96BBB58C /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + B68C3009456C69A3E0E737B9 /* Pods */, + 977B1D48C0E48E96D51532D2 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + B68C3009456C69A3E0E737B9 /* Pods */ = { + isa = PBXGroup; + children = ( + 026117FF47F6FEB1E07D9481 /* Pods-Runner.debug.xcconfig */, + 4F4E4CE4EAC8F5376068C322 /* Pods-Runner.release.xcconfig */, + 1B25BBAEEA2C12CCC61636D4 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 1EDE0E94DBE71CD083227E90 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 74E1176F028703D33774BD4F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1EDE0E94DBE71CD083227E90 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 74E1176F028703D33774BD4F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V92CX3HWGS; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.imageGallerySaverExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V92CX3HWGS; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.imageGallerySaverExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V92CX3HWGS; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.imageGallerySaverExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..6a028eb --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + image_gallery_saver_example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + sandbox_fox_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSPhotoLibraryAddUsageDescription + + NSPhotoLibraryUsageDescription + 获取相册权限 + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/lib/dialog.dart b/example/lib/dialog.dart new file mode 100644 index 0000000..aafdc89 --- /dev/null +++ b/example/lib/dialog.dart @@ -0,0 +1,194 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'style.dart'; + +class HDialog { + static Future show( + {required BuildContext context, + String title = '温馨提示', + String? content, + bool barrierDismissible = true, + Color? barrierColor = Colors.black54, + bool? useSafeArea, + bool? useRootNavigator, + Widget? contentWidget, + List options = const [DialogAction(text: '知道了')]}) { + return showDialog( + context: context, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + useSafeArea: useSafeArea ?? true, + useRootNavigator: useRootNavigator ?? true, + builder: (BuildContext context) => HDialogWidget( + title: title, + content: content, + contentWidget: contentWidget, + options: options, + isSheetStyle: false, + )); + } +} + +class DialogAction { + final String? text; + final ActionType type; + final Widget? child; + final T? actionValue; + final VoidCallback? onPressed; + + const DialogAction( + {this.text, + this.type = ActionType.positive, + this.child, + this.actionValue, + this.onPressed}); +} + +enum ActionType { delete, positive, negative } + +Color _getActionColor(ActionType type) { + switch (type) { + case ActionType.delete: + return HColors.secondary; + case ActionType.negative: + return HColors.mediumGrey; + case ActionType.positive: + return HColors.primary; + default: + return HColors.primary; + } +} + +class HDialogWidget extends StatelessWidget { + final String? title; + final String? content; + final Widget? contentWidget; + final List> options; + final DialogAction? bottomSheetCancel; + final bool isSheetStyle; + + const HDialogWidget( + {Key? key, + this.title, + this.content, + this.contentWidget, + this.options = const [], + this.bottomSheetCancel, + this.isSheetStyle = false}) + : super(key: key); + + @override + Widget build(BuildContext context) { + if (Platform.isIOS) { + return createIOSDialog( + content: content, + contentWidget: contentWidget, + title: title, + context: context, + options: options); + } else { + return createAndroidDialog( + content: content, + contentWidget: contentWidget, + title: title, + context: context, + options: options); + } + } + + AlertDialog createAndroidDialog( + {required BuildContext context, + String? title, + String? content, + required List> options, + Widget? contentWidget}) { + final actions = options.map((option) { + return CupertinoButton( + onPressed: () { + Navigator.pop(context, option.actionValue); + if (option.onPressed != null) option.onPressed!(); + }, + child: option.child ?? + Text(option.text ?? '', + style: TextStyle(color: _getActionColor(option.type)))); + }).toList(); + final dialog = AlertDialog( + title: title==null ? null : Text(title), + content: contentWidget != null && content==null + ? contentWidget + : Text(content ?? ''), + actions: actions, + ); + return dialog; + } + + Widget createIOSDialog( + {required BuildContext context, + String? title, + String? content, + Widget? contentWidget, + required List> options}) { + final actions = options.map((option) { + return CupertinoButton( + onPressed: () { + Navigator.pop(context, option.actionValue); + if (option.onPressed != null) option.onPressed!(); + }, + child: option.child ?? + Text(option.text ?? '', + style: TextStyle(color: _getActionColor(option.type))), + ); + }).toList(); + return CupertinoAlertDialog( + title: title==null ? null : Text(title), + content: contentWidget != null && content == null + ? contentWidget + : Text(content ?? ''), + actions: actions, + ); + } + + Widget createIOSSheetDialog( + {required BuildContext context, + String? title, + String? content, + Widget? contentWidget, + required List> options, + DialogAction? bottomSheetCancel}) { + final actions = options.map((option) { + return CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context, option.actionValue); + if (option.onPressed != null) option.onPressed!(); + }, + child: option.child ?? + Text(option.text ?? '', + style: TextStyle(color: _getActionColor(option.type)))); + }).toList(); + final cancelButton = bottomSheetCancel == null + ? null + : CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context, bottomSheetCancel.actionValue); + if (bottomSheetCancel.onPressed != null) + bottomSheetCancel.onPressed!(); + }, + isDefaultAction: true, + child: bottomSheetCancel.child ?? + Text(bottomSheetCancel.text ?? '', + style: TextStyle( + color: _getActionColor(bottomSheetCancel.type))), + ); + return CupertinoActionSheet( + title: title==null ? null : Text(title), + message: contentWidget != null && content==null + ? contentWidget + : Text(content ?? ''), + actions: actions, + cancelButton: cancelButton); + } +} \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..6c5c87d --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,151 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'utils.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Save image to gallery', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + GlobalKey _globalKey = GlobalKey(); + + @override + void initState() { + super.initState(); + PermissionUtil.requestAll(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Save image to gallery"), + ), + body: Center( + child: Column( + children: [ + SizedBox(height: 15), + RepaintBoundary( + key: _globalKey, + child: Container( + alignment: Alignment.center, + width: 300, + height: 300, + color: Colors.blue, + ), + ), + Container( + padding: EdgeInsets.only(top: 15), + child: ElevatedButton( + onPressed: _saveLocalImage, + child: Text("Save Local Image"), + ), + width: 300, + height: 44, + ), + Container( + padding: EdgeInsets.only(top: 15), + child: ElevatedButton( + onPressed: _saveNetworkImage, + child: Text("Save Network Image"), + ), + width: 300, + height: 44, + ), + Container( + padding: EdgeInsets.only(top: 15), + child: ElevatedButton( + onPressed: _saveNetworkGifFile, + child: Text("Save Network Gif Image"), + ), + width: 300, + height: 44, + ), + Container( + padding: EdgeInsets.only(top: 15), + child: ElevatedButton( + onPressed: _saveNetworkVideoFile, + child: Text("Save Network Video"), + ), + width: 300, + height: 44, + ), + ], + ), + )); + } + + _saveLocalImage() async { + RenderRepaintBoundary boundary = + _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; + ui.Image image = await boundary.toImage(); + ByteData? byteData = + await (image.toByteData(format: ui.ImageByteFormat.png)); + if (byteData != null) { + final result = + await ImageGallerySaver.saveImage(byteData.buffer.asUint8List()); + print(result); + Utils.toast(result.toString()); + } + } + + _saveNetworkImage() async { + var response = await Dio().get( + "https://ss0.baidu.com/94o3dSag_xI4khGko9WTAnF6hhy/image/h%3D300/sign=a62e824376d98d1069d40a31113eb807/838ba61ea8d3fd1fc9c7b6853a4e251f94ca5f46.jpg", + options: Options(responseType: ResponseType.bytes)); + final result = await ImageGallerySaver.saveImage( + Uint8List.fromList(response.data), + quality: 60, + name: "hello"); + print(result); + Utils.toast("$result"); + } + + _saveNetworkGifFile() async { + var appDocDir = await getTemporaryDirectory(); + String savePath = appDocDir.path + "/temp.gif"; + String fileUrl = + "https://hyjdoc.oss-cn-beijing.aliyuncs.com/hyj-doc-flutter-demo-run.gif"; + await Dio().download(fileUrl, savePath); + final result = + await ImageGallerySaver.saveFile(savePath, isReturnPathOfIOS: true); + print(result); + Utils.toast("$result"); + } + + _saveNetworkVideoFile() async { + var appDocDir = await getTemporaryDirectory(); + String savePath = appDocDir.path + "/temp.mp4"; + String fileUrl = + "https://s3.cn-north-1.amazonaws.com.cn/mtab.kezaihui.com/video/ForBiggerBlazes.mp4"; + await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) { + print((count / total * 100).toStringAsFixed(0) + "%"); + }); + final result = await ImageGallerySaver.saveFile(savePath); + print(result); + Utils.toast("$result"); + } +} diff --git a/example/lib/style.dart b/example/lib/style.dart new file mode 100644 index 0000000..b66fc14 --- /dev/null +++ b/example/lib/style.dart @@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class HFontSizes { + static const normal = 16.0; + static const normal2 = 15.0; + static const small = 14.0; + static const smaller = 13.0; +} + +extension FontWeightExt on FontWeight { + static const FontWeight light = FontWeight.w300; + static const FontWeight regular = FontWeight.w400; + static FontWeight medium = Platform.isAndroid ? FontWeight.w600 : FontWeight.w600; + static const FontWeight semibold = FontWeight.w800; + static const FontWeight bold = FontWeight.w900; +} + +class HColors { + static const Color contentBackgroundColor = Colors.white; + static const Color contentBackgroundGrayColor2 = Color(0xFFF8F8F8); + static const Color secondaryLabelColor2 = Color(0xFF666666); + static const Color secondaryLabelColor3 = Color(0xFF999999); + static const Color secondaryLabelColor4 = Color(0xFFB4B4B4); + static const Color blue2 = Color(0xFF0091FF); + static const Color primary = Color(0xFF448AFF); + static const Color secondary = Color(0xFFE95E3D); + static const Color mediumGrey = Color(0xFF86919C); +} diff --git a/example/lib/utils.dart b/example/lib/utils.dart new file mode 100644 index 0000000..e22e5a6 --- /dev/null +++ b/example/lib/utils.dart @@ -0,0 +1,81 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'dialog.dart'; + +class Utils { + static void toast(String msg) { + Fluttertoast.showToast( + msg: msg, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + textColor: Colors.white, + fontSize: 16.0, + backgroundColor: Colors.black, + ); + } +} + +class PermissionUtil { + /// 安卓权限 + static List androidPermissions = [ + // 在这里添加需要的权限 + Permission.storage + ]; + + /// ios权限 + static List iosPermissions = [ + // 在这里添加需要的权限 + Permission.storage + ]; + + static Future> requestAll() async { + if (Platform.isIOS) { + return await iosPermissions.request(); + } + return await androidPermissions.request(); + } + + static Future> request( + Permission permission) async { + final List permissions = [permission]; + return await permissions.request(); + } + + static bool isDenied(Map result) { + var isDenied = false; + result.forEach((key, value) { + if (value == PermissionStatus.denied) { + isDenied = true; + return; + } + }); + return isDenied; + } + + static void showDeniedDialog(BuildContext context) { + HDialog.show( + context: context, + title: '权限申请异常', + content: '请在【应用信息】-【权限管理】中,开启全部所需权限,以正常使用惠爆单功能', + options: [ + DialogAction(text: '去设置', onPressed: () => openAppSettings()) + ]); + } + + /// 检查权限 + static Future checkGranted(Permission permission) async { + PermissionStatus storageStatus = await permission.status; + if (storageStatus == PermissionStatus.granted) { + //已授权 + return true; + } else { + //拒绝授权 + return false; + } + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..beb6049 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,73 @@ +name: image_gallery_saver_example +description: Demonstrates how to use the image_gallery_saver plugin. + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# Read more about versioning at semver.org. +version: 2.0.3+3 + +environment: + sdk: '>=2.19.6 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + permission_handler: ^10.3.0 + fluttertoast: ^8.2.2 + path_provider: ^2.0.15 + dio: ^5.2.1+1 + + image_gallery_saver: + path: ../ + cupertino_icons: ^1.0.5 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.io/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.io/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.io/custom-fonts/#from-packages diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..95fbd2f --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_gallery_saver_example/main.dart'; + +void main() { + testWidgets('Verify Widgets', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(new MyApp()); + final Finder flatButtonPass = find.widgetWithText(ElevatedButton, '保存屏幕截图'); + expect(flatButtonPass, findsOneWidget); + }); +} diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..710ec6c --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,36 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ios/Classes/ImageGallerySaverPlugin.h b/ios/Classes/ImageGallerySaverPlugin.h new file mode 100644 index 0000000..39b96a0 --- /dev/null +++ b/ios/Classes/ImageGallerySaverPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface ImageGallerySaverPlugin : NSObject +@end diff --git a/ios/Classes/ImageGallerySaverPlugin.m b/ios/Classes/ImageGallerySaverPlugin.m new file mode 100644 index 0000000..a9e65e3 --- /dev/null +++ b/ios/Classes/ImageGallerySaverPlugin.m @@ -0,0 +1,13 @@ +#import "ImageGallerySaverPlugin.h" + +#if __has_include() +#import +#else +#import "image_gallery_saver-Swift.h" +#endif + +@implementation ImageGallerySaverPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftImageGallerySaverPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/ios/Classes/SwiftImageGallerySaverPlugin.swift b/ios/Classes/SwiftImageGallerySaverPlugin.swift new file mode 100644 index 0000000..876c318 --- /dev/null +++ b/ios/Classes/SwiftImageGallerySaverPlugin.swift @@ -0,0 +1,194 @@ +import Flutter +import UIKit +import Photos + +public class SwiftImageGallerySaverPlugin: NSObject, FlutterPlugin { + let errorMessage = "Failed to save, please check whether the permission is enabled" + + var result: FlutterResult?; + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "image_gallery_saver", binaryMessenger: registrar.messenger()) + let instance = SwiftImageGallerySaverPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + self.result = result + if call.method == "saveImageToGallery" { + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + guard let imageData = (arguments["imageBytes"] as? FlutterStandardTypedData)?.data, + let image = UIImage(data: imageData), + let quality = arguments["quality"] as? Int, + let _ = arguments["name"], + let isReturnImagePath = arguments["isReturnImagePathOfIOS"] as? Bool + else { return } + let newImage = image.jpegData(compressionQuality: CGFloat(quality / 100))! + saveImage(UIImage(data: newImage) ?? image, isReturnImagePath: isReturnImagePath) + } else if (call.method == "saveFileToGallery") { + guard let arguments = call.arguments as? [String: Any], + let path = arguments["file"] as? String, + let _ = arguments["name"], + let isReturnFilePath = arguments["isReturnPathOfIOS"] as? Bool else { return } + if (isImageFile(filename: path)) { + saveImageAtFileUrl(path, isReturnImagePath: isReturnFilePath) + } else { + if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(path)) { + saveVideo(path, isReturnImagePath: isReturnFilePath) + }else{ + self.saveResult(isSuccess:false,error:self.errorMessage) + } + } + } else { + result(FlutterMethodNotImplemented) + } + } + + func saveVideo(_ path: String, isReturnImagePath: Bool) { + if !isReturnImagePath { + UISaveVideoAtPathToSavedPhotosAlbum(path, self, #selector(didFinishSavingVideo(videoPath:error:contextInfo:)), nil) + return + } + var videoIds: [String] = [] + + PHPhotoLibrary.shared().performChanges( { + let req = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL.init(fileURLWithPath: path)) + if let videoId = req?.placeholderForCreatedAsset?.localIdentifier { + videoIds.append(videoId) + } + }, completionHandler: { [unowned self] (success, error) in + DispatchQueue.main.async { + if (success && videoIds.count > 0) { + let assetResult = PHAsset.fetchAssets(withLocalIdentifiers: videoIds, options: nil) + if (assetResult.count > 0) { + let videoAsset = assetResult[0] + PHImageManager().requestAVAsset(forVideo: videoAsset, options: nil) { (avurlAsset, audioMix, info) in + if let urlStr = (avurlAsset as? AVURLAsset)?.url.absoluteString { + self.saveResult(isSuccess: true, filePath: urlStr) + } + } + } + } else { + self.saveResult(isSuccess: false, error: self.errorMessage) + } + } + }) + } + + func saveImage(_ image: UIImage, isReturnImagePath: Bool) { + if !isReturnImagePath { + UIImageWriteToSavedPhotosAlbum(image, self, #selector(didFinishSavingImage(image:error:contextInfo:)), nil) + return + } + + var imageIds: [String] = [] + + PHPhotoLibrary.shared().performChanges( { + let req = PHAssetChangeRequest.creationRequestForAsset(from: image) + if let imageId = req.placeholderForCreatedAsset?.localIdentifier { + imageIds.append(imageId) + } + }, completionHandler: { [unowned self] (success, error) in + DispatchQueue.main.async { + if (success && imageIds.count > 0) { + let assetResult = PHAsset.fetchAssets(withLocalIdentifiers: imageIds, options: nil) + if (assetResult.count > 0) { + let imageAsset = assetResult[0] + let options = PHContentEditingInputRequestOptions() + options.canHandleAdjustmentData = { (adjustmeta) + -> Bool in true } + imageAsset.requestContentEditingInput(with: options) { [unowned self] (contentEditingInput, info) in + if let urlStr = contentEditingInput?.fullSizeImageURL?.absoluteString { + self.saveResult(isSuccess: true, filePath: urlStr) + } + } + } + } else { + self.saveResult(isSuccess: false, error: self.errorMessage) + } + } + }) + } + + func saveImageAtFileUrl(_ url: String, isReturnImagePath: Bool) { + if !isReturnImagePath { + if let image = UIImage(contentsOfFile: url) { + UIImageWriteToSavedPhotosAlbum(image, self, #selector(didFinishSavingImage(image:error:contextInfo:)), nil) + } + return + } + + var imageIds: [String] = [] + + PHPhotoLibrary.shared().performChanges( { + let req = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: URL(string: url)!) + if let imageId = req?.placeholderForCreatedAsset?.localIdentifier { + imageIds.append(imageId) + } + }, completionHandler: { [unowned self] (success, error) in + DispatchQueue.main.async { + if (success && imageIds.count > 0) { + let assetResult = PHAsset.fetchAssets(withLocalIdentifiers: imageIds, options: nil) + if (assetResult.count > 0) { + let imageAsset = assetResult[0] + let options = PHContentEditingInputRequestOptions() + options.canHandleAdjustmentData = { (adjustmeta) + -> Bool in true } + imageAsset.requestContentEditingInput(with: options) { [unowned self] (contentEditingInput, info) in + if let urlStr = contentEditingInput?.fullSizeImageURL?.absoluteString { + self.saveResult(isSuccess: true, filePath: urlStr) + } + } + } + } else { + self.saveResult(isSuccess: false, error: self.errorMessage) + } + } + }) + } + + /// finish saving,if has error,parameters error will not nill + @objc func didFinishSavingImage(image: UIImage, error: NSError?, contextInfo: UnsafeMutableRawPointer?) { + saveResult(isSuccess: error == nil, error: error?.description) + } + + @objc func didFinishSavingVideo(videoPath: String, error: NSError?, contextInfo: UnsafeMutableRawPointer?) { + saveResult(isSuccess: error == nil, error: error?.description) + } + + func saveResult(isSuccess: Bool, error: String? = nil, filePath: String? = nil) { + var saveResult = SaveResultModel() + saveResult.isSuccess = error == nil + saveResult.errorMessage = error?.description + saveResult.filePath = filePath + result?(saveResult.toDic()) + } + + func isImageFile(filename: String) -> Bool { + return filename.hasSuffix(".jpg") + || filename.hasSuffix(".png") + || filename.hasSuffix(".jpeg") + || filename.hasSuffix(".JPEG") + || filename.hasSuffix(".JPG") + || filename.hasSuffix(".PNG") + || filename.hasSuffix(".gif") + || filename.hasSuffix(".GIF") + || filename.hasSuffix(".heic") + || filename.hasSuffix(".HEIC") + } +} + +public struct SaveResultModel: Encodable { + var isSuccess: Bool! + var filePath: String? + var errorMessage: String? + + func toDic() -> [String:Any]? { + let encoder = JSONEncoder() + guard let data = try? encoder.encode(self) else { return nil } + if (!JSONSerialization.isValidJSONObject(data)) { + return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any] + } + return nil + } +} diff --git a/ios/image_gallery_saver.podspec b/ios/image_gallery_saver.podspec new file mode 100644 index 0000000..e27e6d1 --- /dev/null +++ b/ios/image_gallery_saver.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'image_gallery_saver' + s.version = '2.0.2' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' + s.swift_version = '5.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } +end + diff --git a/lib/image_gallery_saver.dart b/lib/image_gallery_saver.dart new file mode 100644 index 0000000..ffb12f2 --- /dev/null +++ b/lib/image_gallery_saver.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +class ImageGallerySaver { + static const MethodChannel _channel = + const MethodChannel('image_gallery_saver'); + + /// save image to Gallery + /// imageBytes can't null + /// return Map type + /// for example:{"isSuccess":true, "filePath":String?} + static FutureOr saveImage(Uint8List imageBytes, + {int quality = 80, + String? name, + bool isReturnImagePathOfIOS = false}) async { + final result = + await _channel.invokeMethod('saveImageToGallery', { + 'imageBytes': imageBytes, + 'quality': quality, + 'name': name, + 'isReturnImagePathOfIOS': isReturnImagePathOfIOS + }); + return result; + } + + /// Save the PNG,JPG,JPEG image or video located at [file] to the local device media gallery. + static Future saveFile(String file, + {String? name, bool isReturnPathOfIOS = false}) async { + final result = await _channel.invokeMethod( + 'saveFileToGallery', { + 'file': file, + 'name': name, + 'isReturnPathOfIOS': isReturnPathOfIOS + }); + return result; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..9ca1581 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,31 @@ +name: image_gallery_saver +description: A flutter plugin for save image to gallery, iOS need to add the following keys to your Info.plist file. +version: 2.0.3 +homepage: https://github.com/hui-z/image_gallery_saver + +environment: + sdk: '>=2.12.0 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + plugin: + platforms: + android: + package: com.example.imagegallerysaver + pluginClass: ImageGallerySaverPlugin + ios: + pluginClass: ImageGallerySaverPlugin + +dev_dependencies: + flutter_test: + sdk: flutter + diff --git a/test/image_gallery_saver_test.dart b/test/image_gallery_saver_test.dart new file mode 100644 index 0000000..5faca52 --- /dev/null +++ b/test/image_gallery_saver_test.dart @@ -0,0 +1,42 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const MethodChannel channel = MethodChannel('image_gallery_saver'); + final List log = []; + bool? response; + + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return response; + }); + + tearDown(() { + log.clear(); + }); + + + test('saveImageToGallery test', () async { + response = true; + Uint8List imageBytes = Uint8List(16); + final bool? result = await (ImageGallerySaver.saveImage(imageBytes) as FutureOr); + expect( + log, + [ + isMethodCall('saveImageToGallery', arguments: { + 'imageBytes': imageBytes, + 'quality': 80, + 'name': null, + "isReturnImagePathOfIOS": false + }) + ], + ); + expect(result, response); + }); + +}