initial commit

This commit is contained in:
Wenqi Li 2020-08-14 18:07:27 +08:00
commit 67d2a5f8a2
98 changed files with 4753 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.DS_Store .dart_tool/ .packages .pub/ build/ .idea/ *.iml

10
.metadata Normal file
View File

@ -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: 8af6b2f038c1172e61d418869363a28dffec3cb4
channel: stable
project_type: plugin

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
## 0.1.0
Initial release.

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2020, Wenqi Li
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

174
README.md Normal file
View File

@ -0,0 +1,174 @@
# Photo Gallery
A Flutter plugin that retrieves images and videos from mobile native gallery.
## Installation
First, add photo_gallery as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/packages-and-plugins/using-packages).
#### iOS
Add the following keys to your *Info.plist* file, located in ```<project root>/ios/Runner/Info.plist```:
```NSPhotoLibraryUsageDescription``` - describe why your app needs permission for the photo library. This is called *Privacy - Photo Library Usage Description* in the visual editor.
```xml
<key>NSPhotoLibraryUsageDescription</key>
<string>Example usage description</string>
```
#### Android
Add the following permissions to your *AndroidManifest.xml*, located in ```<project root>/android/app/src/main/AndroidManifest.xml```:
```xml
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
<manifest/>
```
API 29+
Add the following property to your *AndroidManifest.xml*, located in ```<project root>/android/app/src/main/AndroidManifest.xml``` to [opt-out of scoped storage](https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage):
```xml
<manifest ...>
...
<application
android:requestLegacyExternalStorage="true"
...>
<application/>
<manifest/>
```
## Usage
* Listing albums in the gallery
```dart
final List<Album> imageAlbums = await PhotoGallery.listAlbums(
mediumType: mediumType.image,
);
final List<Album> videoAlbums = await PhotoGallery.listAlbums(
mediumType: mediumType.video,
);
```
* Listing media in an album
```dart
final MediaPage imagePage = await imageAlbum.listMedia(
skip: 5,
take: 10,
);
final MediaPage videoPage = await videoAlbum.listMedia(
skip: 5,
take: 10,
);
final List<Medium> allMedia = [
...imagePage.items,
...videoPage.items,
];
```
* Loading more media in a album
```dart
if (!imagePage.isLast) {
final nextImagePage = await imagePage.nextPage();
// ...
}
```
* Getting a file
```dart
final File file = await medium.getFile();
```
```dart
final File file = await PhotoGallery.getFile(mediumId: mediumId);
```
* Getting thumbnail data
```dart
final List<int> data = await medium.getThumbnail();
```
```dart
final List<int> data = await PhotoGallery.getThumbnail(mediumId: mediumId);
```
You can also specify thumbnail width and height on Android API 21 or higher; You can also specify thumbnail width, height and whether provider high quality or not on iOS:
```dart
final List<int> data = await medium.getThumbnail(
width: 128,
height: 128,
highQuality: true,
);
```
```dart
final List<int> data = await PhotoGallery.getThumbnail(
mediumId: mediumId,
width: 128,
height: 128,
highQuality: true,
);
```
* Getting album thumbnail data
```dart
final List<int> data = await album.getThumbnail();
```
```dart
final List<int> data = await PhotoGallery.getAlbumThumbnail(albumId: albumId);
```
You can also specify thumbnail width and height on Android API 21 or higher; You can also specify thumbnail width, height and whether provider high quality or not on iOS:
```dart
final List<int> data = await album.getThumbnail(
width: 128,
height: 128,
highQuality: true,
);
```
```dart
final List<int> data = await PhotoGallery.getAlbumThumbnail(
albumId: albumId,
width: 128,
height: 128,
highQuality: true,
);
```
* Displaying medium thumbnail
ThumbnailProvider are available to display thumbnail images (here with the help of dependency [transparent_image](https://pub.dev/packages/transparent_image)):
```dart
FadeInImage(
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: ThumbnailProvider(
mediumId: mediumId,
width: 128,
height: 128,
hightQuality: true,
),
)
```
Width and height is only available on Android API 29+ or iOS platform
* Displaying album thumbnail
AlbumThumbnailProvider are available to display album thumbnail images (here with the help of dependency [transparent_image](https://pub.dev/packages/transparent_image)):
```dart
FadeInImage(
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: AlbumThumbnailProvider(
albumId: albumId,
width: 128,
height: 128,
hightQuality: true,
),
)
```
Width and height is only available on Android API 21+ or iOS platform
* Displaying a full size image
You can use PhotoProvider to display the full size image (here with the help of dependency [transparent_image](https://pub.dev/packages/transparent_image)):
```dart
FadeInImage(
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: PhotoProvider(
mediumId: mediumId,
),
)
```

8
android/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures

43
android/build.gradle Normal file
View File

@ -0,0 +1,43 @@
group 'com.morbit.photogallery'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.3.72'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
rootProject.allprojects {
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 29
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 16
}
lintOptions {
disable 'InvalidPackage'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip

160
android/gradlew vendored Executable file
View File

@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# 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
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# 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
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" ] ; 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
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
android/gradlew.bat vendored Normal file
View File

@ -0,0 +1,90 @@
@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
@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=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@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 Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_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=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
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

1
android/settings.gradle Normal file
View File

@ -0,0 +1 @@
rootProject.name = 'photo_gallery'

View File

@ -0,0 +1 @@
<manifest package="com.morbit.photogallery"></manifest>

View File

@ -0,0 +1,623 @@
package com.morbit.photogallery
import android.content.ContentUris
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
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 android.graphics.Bitmap
import java.io.ByteArrayOutputStream
import android.provider.MediaStore
import android.content.Context
import android.database.Cursor
import android.database.Cursor.FIELD_TYPE_INTEGER
import android.os.AsyncTask
import android.os.Build
import android.util.Size
/** PhotoGalleryPlugin */
class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val channel = MethodChannel(flutterPluginBinding.binaryMessenger, "photo_gallery")
val plugin = PhotoGalleryPlugin()
plugin.context = flutterPluginBinding.applicationContext
channel.setMethodCallHandler(plugin)
}
companion object {
// This static function is optional and equivalent to onAttachedToEngine. It supports the old
// pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
// plugin registration via this function while apps migrate to use the new Android APIs
// post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
//
// It is encouraged to share logic between onAttachedToEngine and registerWith to keep
// them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
// depending on the user's project. onAttachedToEngine or registerWith must both be defined
// in the same class.
@JvmStatic
fun registerWith(registrar: Registrar) {
val channel = MethodChannel(registrar.messenger(), "photo_gallery")
val plugin = PhotoGalleryPlugin()
plugin.context = registrar.activeContext()
channel.setMethodCallHandler(plugin)
}
const val imageType = "image"
const val videoType = "video"
const val allAlbumId = "__ALL__"
const val allAlbumName = "All"
val imageMetadataProjection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_MODIFIED
)
val videoMetadataProjection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.WIDTH,
MediaStore.Video.Media.HEIGHT,
MediaStore.Video.Media.DATE_TAKEN,
MediaStore.Video.Media.DATE_MODIFIED
)
const val imageOrderBy = "${MediaStore.Images.Media.DATE_TAKEN} DESC, ${MediaStore.Images.Media.DATE_MODIFIED} DESC"
const val videoOrderBy = "${MediaStore.Video.Media.DATE_TAKEN} DESC, ${MediaStore.Video.Media.DATE_MODIFIED} DESC"
}
private var context: Context? = null
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"listAlbums" -> {
val mediumType = call.argument<String>("mediumType")
BackgroundAsyncTask({
listAlbums(mediumType!!)
}, { v ->
result.success(v)
})
}
"listMedia" -> {
val albumId = call.argument<String>("albumId")
val mediumType = call.argument<String>("mediumType")
val total = call.argument<Int>("total")
val skip = call.argument<Int>("skip")
val take = call.argument<Int>("take")
BackgroundAsyncTask({
when (mediumType) {
imageType -> listImages(albumId!!, total!!, skip, take)
videoType -> listVideos(albumId!!, total!!, skip, take)
else -> null
}
}, { v ->
result.success(v)
})
}
"getMedium" -> {
val mediumId = call.argument<String>("mediumId")
val mediumType = call.argument<String>("mediumType")
BackgroundAsyncTask({
getMedium(mediumId!!, mediumType)
}, { v ->
result.success(v)
})
}
"getThumbnail" -> {
val mediumId = call.argument<String>("mediumId")
val mediumType = call.argument<String>("mediumType")
val width = call.argument<Int>("width")
val height = call.argument<Int>("height")
BackgroundAsyncTask({
getThumbnail(mediumId!!, mediumType, width, height)
}, { v ->
result.success(v)
})
}
"getAlbumThumbnail" -> {
val albumId = call.argument<String>("albumId")
val width = call.argument<Int>("width")
val height = call.argument<Int>("height")
BackgroundAsyncTask({
getAlbumThumbnail(albumId!!, width, height)
}, { v ->
result.success(v)
})
}
"getFile" -> {
val mediumId = call.argument<String>("mediumId")
val mediumType = call.argument<String>("mediumType")
BackgroundAsyncTask({
getFile(mediumId!!, mediumType)
}, { v ->
result.success(v)
})
}
else -> result.notImplemented()
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
}
private fun listAlbums(mediumType: String): List<Map<String, Any>> {
return when (mediumType) {
imageType -> {
listImageAlbums()
}
videoType -> {
listVideoAlbums()
}
else -> {
listOf()
}
}
}
private fun listImageAlbums(): List<Map<String, Any>> {
this.context?.run {
var total = 0
val albumHashMap = mutableMapOf<String, MutableMap<String, Any>>()
val imageProjection = arrayOf(
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.BUCKET_ID
)
val imageCursor = this.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
imageProjection,
null,
null,
null
)
imageCursor?.use { cursor ->
val bucketColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
val bucketColumnId = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)
while (cursor.moveToNext()) {
val bucketId = cursor.getString(bucketColumnId)
val album = albumHashMap[bucketId]
if (album == null) {
val folderName = cursor.getString(bucketColumn)
albumHashMap[bucketId] = mutableMapOf(
"id" to bucketId,
"mediumType" to imageType,
"name" to folderName,
"count" to 1
)
} else {
val count = album["count"] as Int
album["count"] = count + 1
}
total++
}
}
val albumList = mutableListOf<Map<String, Any>>()
albumList.add(
mapOf(
"id" to allAlbumId,
"mediumType" to imageType,
"name" to allAlbumName,
"count" to total
)
)
albumList.addAll(albumHashMap.values)
return albumList
}
return listOf()
}
private fun listVideoAlbums(): List<Map<String, Any>> {
this.context?.run {
var total = 0
val albumHashMap = mutableMapOf<String, MutableMap<String, Any>>()
val videoProjection = arrayOf(
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.BUCKET_ID
)
val videoCursor = this.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
videoProjection,
null,
null,
null
)
videoCursor?.use { cursor ->
val bucketColumn = cursor.getColumnIndex(MediaStore.Video.Media.BUCKET_DISPLAY_NAME)
val bucketColumnId = cursor.getColumnIndex(MediaStore.Video.Media.BUCKET_ID)
while (cursor.moveToNext()) {
val bucketId = cursor.getString(bucketColumnId)
val album = albumHashMap[bucketId]
if (album == null) {
val folderName = cursor.getString(bucketColumn)
albumHashMap[bucketId] = mutableMapOf(
"id" to bucketId,
"mediumType" to videoType,
"name" to folderName,
"count" to 1
)
} else {
val count = album["count"] as Int
album["count"] = count + 1
}
total++
}
}
val albumList = mutableListOf<Map<String, Any>>()
albumList.add(mapOf(
"id" to allAlbumId,
"mediumType" to videoType,
"name" to allAlbumName,
"count" to total))
albumList.addAll(albumHashMap.values)
return albumList
}
return listOf()
}
private fun listImages(albumId: String, total: Int, skip: Int?, take: Int?): Map<String, Any> {
val media = mutableListOf<Map<String, Any?>>()
val offset = skip ?: 0
val limit = take ?: (total - offset)
this.context?.run {
val imageCursor = this.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
imageMetadataProjection,
if (albumId == allAlbumId) null else "${MediaStore.Images.Media.BUCKET_ID} = $albumId",
null,
"$imageOrderBy LIMIT $limit OFFSET $offset"
)
imageCursor?.use { cursor ->
while (cursor.moveToNext()) {
media.add(getImageMetadata(cursor))
}
}
}
return mapOf(
"start" to offset,
"total" to total,
"items" to media
)
}
private fun listVideos(albumId: String, total: Int, skip: Int?, take: Int?): Map<String, Any> {
val media = mutableListOf<Map<String, Any?>>()
val offset = skip ?: 0
val limit = take ?: (total - offset)
this.context?.run {
val videoCursor = this.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
videoMetadataProjection,
if (albumId == allAlbumId) null else "${MediaStore.Images.Media.BUCKET_ID} = $albumId",
null,
"$videoOrderBy LIMIT $limit OFFSET $offset")
videoCursor?.use { cursor ->
while (cursor.moveToNext()) {
media.add(getVideoMetadata(cursor))
}
}
}
return mapOf(
"start" to offset,
"total" to total,
"items" to media
)
}
private fun getMedium(mediumId: String, mediumType: String?): Map<String, Any?>? {
return when (mediumType) {
imageType -> {
getImageMedia(mediumId)
}
videoType -> {
getVideoMedia(mediumId)
}
else -> {
getImageMedia(mediumId) ?: getVideoMedia(mediumId)
}
}
}
private fun getImageMedia(mediumId: String): Map<String, Any?>? {
var imageMetadata: Map<String, Any?>? = null
this.context?.run {
val imageCursor = this.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
imageMetadataProjection,
"${MediaStore.Images.Media._ID} = $mediumId",
null,
null
)
imageCursor?.use { cursor ->
if (cursor.moveToFirst()) {
imageMetadata = getImageMetadata(cursor)
}
}
}
return imageMetadata
}
private fun getVideoMedia(mediumId: String): Map<String, Any?>? {
var videoMetadata: Map<String, Any?>? = null
this.context?.run {
val videoCursor = this.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
videoMetadataProjection,
"${MediaStore.Images.Media._ID} = $mediumId",
null,
null)
videoCursor?.use { cursor ->
if (cursor.moveToFirst()) {
videoMetadata = getVideoMetadata(cursor)
}
}
}
return videoMetadata
}
private fun getThumbnail(mediumId: String, mediumType: String?, width: Int?, height: Int?): ByteArray? {
return when (mediumType) {
imageType -> {
getImageThumbnail(mediumId, width, height)
}
videoType -> {
getVideoThumbnail(mediumId, width, height)
}
else -> {
getImageThumbnail(mediumId, width, height)
?: getVideoThumbnail(mediumId, width, height)
}
}
}
private fun getImageThumbnail(mediumId: String, width: Int?, height: Int?): ByteArray? {
var byteArray: ByteArray? = null
val bitmap: Bitmap? = this.context?.run {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.contentResolver.loadThumbnail(
ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediumId.toLong()),
Size(width ?: 72, height ?: 72),
null
)
} else {
MediaStore.Images.Thumbnails.getThumbnail(
this.contentResolver, mediumId.toLong(),
MediaStore.Images.Thumbnails.MINI_KIND,
null
)
}
}
bitmap?.run {
ByteArrayOutputStream().use { stream ->
this.compress(Bitmap.CompressFormat.JPEG, 100, stream)
byteArray = stream.toByteArray()
}
}
return byteArray
}
private fun getVideoThumbnail(mediumId: String, width: Int?, height: Int?): ByteArray? {
var byteArray: ByteArray? = null
val bitmap: Bitmap? = this.context?.run {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.contentResolver.loadThumbnail(
ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediumId.toLong()),
Size(width ?: 72, height ?: 72),
null
)
} else {
MediaStore.Video.Thumbnails.getThumbnail(
this.contentResolver, mediumId.toLong(),
MediaStore.Images.Thumbnails.MINI_KIND,
null
)
}
}
bitmap?.run {
ByteArrayOutputStream().use { stream ->
this.compress(Bitmap.CompressFormat.JPEG, 100, stream)
byteArray = stream.toByteArray()
}
}
return byteArray
}
private fun getAlbumThumbnail(albumId: String, width: Int?, height: Int?): ByteArray? {
return this.context?.run {
val imageCursor = this.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Images.Media._ID),
if (albumId == allAlbumId) null else "${MediaStore.Images.Media.BUCKET_ID} = $albumId",
null,
MediaStore.Images.Media.DATE_TAKEN + " DESC LIMIT 1"
)
imageCursor?.use { cursor ->
if (cursor.moveToFirst()) {
val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
val id = cursor.getLong(idColumn)
return@run getImageThumbnail(id.toString(), width, height)
}
}
val videoCursor = this.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Video.Media._ID),
if (albumId == allAlbumId) null else "${MediaStore.Video.Media.BUCKET_ID} = $albumId",
null,
MediaStore.Video.Media.DATE_TAKEN + " DESC LIMIT 1"
)
videoCursor?.use { cursor ->
if (cursor.moveToNext()) {
val idColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID)
val id = cursor.getLong(idColumn)
return@run getVideoThumbnail(id.toString(), width, height)
}
}
return@run null
}
}
private fun getFile(mediumId: String, mediumType: String?): String? {
return when (mediumType) {
imageType -> {
getImageFile(mediumId)
}
videoType -> {
getVideoFile(mediumId)
}
else -> {
getImageFile(mediumId) ?: getVideoFile(mediumId)
}
}
}
private fun getImageFile(mediumId: String): String? {
var path: String? = null
this.context?.run {
val imageCursor = this.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Images.Media.DATA),
"${MediaStore.Images.Media._ID} = $mediumId",
null,
null
)
imageCursor?.use { cursor ->
if (cursor.moveToNext()) {
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
path = cursor.getString(dataColumn)
}
}
}
return path
}
private fun getVideoFile(mediumId: String): String? {
var path: String? = null
this.context?.run {
val videoCursor = this.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Images.Media.DATA),
"${MediaStore.Images.Media._ID} = $mediumId",
null,
null)
videoCursor?.use { cursor ->
if (cursor.moveToNext()) {
val dataColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATA)
path = cursor.getString(dataColumn)
}
}
}
return path
}
private fun getImageMetadata(cursor: Cursor): Map<String, Any?> {
val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
val widthColumn = cursor.getColumnIndex(MediaStore.Images.Media.WIDTH)
val heightColumn = cursor.getColumnIndex(MediaStore.Images.Media.HEIGHT)
val dateTakenColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN)
val dateModifiedColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED)
val id = cursor.getLong(idColumn)
val width = cursor.getLong(widthColumn)
val height = cursor.getLong(heightColumn)
var dateTaken: Long? = null
if (cursor.getType(dateTakenColumn) == FIELD_TYPE_INTEGER) {
dateTaken = cursor.getLong(dateTakenColumn)
}
var dateModified: Long? = null
if (cursor.getType(dateModifiedColumn) == FIELD_TYPE_INTEGER) {
dateModified = cursor.getLong(dateModifiedColumn) * 1000
}
return mapOf(
"id" to id.toString(),
"mediumType" to imageType,
"width" to width,
"height" to height,
"creationDate" to dateTaken,
"modifiedDate" to dateModified
)
}
private fun getVideoMetadata(cursor: Cursor): Map<String, Any?> {
val idColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID)
val widthColumn = cursor.getColumnIndex(MediaStore.Video.Media.WIDTH)
val heightColumn = cursor.getColumnIndex(MediaStore.Video.Media.HEIGHT)
val dateTakenColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATE_TAKEN)
val dateModifiedColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATE_MODIFIED)
val id = cursor.getLong(idColumn)
val width = cursor.getLong(widthColumn)
val height = cursor.getLong(heightColumn)
var dateTaken: Long? = null
if (cursor.getType(dateTakenColumn) == FIELD_TYPE_INTEGER) {
dateTaken = cursor.getLong(dateTakenColumn)
}
var dateModified: Long? = null
if (cursor.getType(dateModifiedColumn) == FIELD_TYPE_INTEGER) {
dateModified = cursor.getLong(dateModifiedColumn) * 1000
}
return mapOf(
"id" to id.toString(),
"mediumType" to videoType,
"width" to width,
"height" to height,
"creationDate" to dateTaken,
"modifiedDate" to dateModified
)
}
}
class BackgroundAsyncTask<T>(val handler: () -> T, val post: (result: T) -> Unit) : AsyncTask<Void, Void, T>() {
init {
execute()
}
override fun doInBackground(vararg params: Void?): T {
return handler()
}
override fun onPostExecute(result: T) {
super.onPostExecute(result)
post(result)
return
}
}

43
example/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

10
example/.metadata Normal file
View File

@ -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: 8af6b2f038c1172e61d418869363a28dffec3cb4
channel: stable
project_type: app

14
example/README.md Normal file
View File

@ -0,0 +1,14 @@
# photo_gallery_example
Demonstrates how to use the photo_gallery plugin.
Use photo_gallery plugin to list images and videos from mobile native gallery. support both iOS and Android (with the help of image_picker plugin).
## Install
```shell script
flutter pub get
```
## Run
```shell script
flutter run
```

7
example/android/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java

View File

@ -0,0 +1,54 @@
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 FileNotFoundException("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 29
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
applicationId "com.morbit.photo_gallery_example"
minSdkVersion 16
targetSdkVersion 29
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.morbit.photo_gallery_example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -0,0 +1,49 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.morbit.photo_gallery_example">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name="io.flutter.app.FlutterApplication"
android:requestLegacyExternalStorage="true"
android:label="photo_gallery_example"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -0,0 +1,6 @@
package com.morbit.photo_gallery_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/white</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.morbit.photo_gallery_example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.3.72'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

View File

@ -0,0 +1,15 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
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"

View File

@ -0,0 +1 @@
include ':app'

32
example/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
*.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/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

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>8.0</string>
</dict>
</plist>

View File

@ -0,0 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

87
example/ios/Podfile Normal file
View File

@ -0,0 +1,87 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
return [];
end
generated_key_values = {}
skip_line_start_symbols = ["#", "/"]
File.foreach(file_abs_path) do |line|
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern=separator)
if plugin.length == 2
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
generated_key_values[podname] = podpath
else
puts "Invalid plugin specification: #{line}"
end
end
generated_key_values
end
target 'Runner' do
use_frameworks!
use_modular_headers!
# Flutter Pod
copied_flutter_dir = File.join(__dir__, 'Flutter')
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
unless File.exist?(generated_xcode_build_settings_path)
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
unless File.exist?(copied_framework_path)
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
end
unless File.exist?(copied_podspec_path)
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
end
end
# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
# Plugin Pods
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
# referring to absolute paths on developers' machines.
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
plugin_pods = parse_KV_file('../.flutter-plugins')
plugin_pods.each do |name, path|
symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
pod name, :path => File.join(symlink, 'ios')
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end

28
example/ios/Podfile.lock Normal file
View File

@ -0,0 +1,28 @@
PODS:
- Flutter (1.0.0)
- "permission_handler (5.0.1+1)":
- Flutter
- photo_gallery (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- permission_handler (from `.symlinks/plugins/permission_handler/ios`)
- photo_gallery (from `.symlinks/plugins/photo_gallery/ios`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
permission_handler:
:path: ".symlinks/plugins/permission_handler/ios"
photo_gallery:
:path: ".symlinks/plugins/photo_gallery/ios"
SPEC CHECKSUMS:
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6
photo_gallery: 9f95e57747cd22c10676ece3660d1ffe6c603ee5
PODFILE CHECKSUM: c34e2287a9ccaa606aeceab922830efb9a6ff69a
COCOAPODS: 1.9.3

View File

@ -0,0 +1,577 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
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 */; };
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 */; };
FC9C4708640E3D61822E65FE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D559FAF83BE8A73D7D2342A6 /* Pods_Runner.framework */; };
/* 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 */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
776376D62B8541C00FA61280 /* 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 = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D559FAF83BE8A73D7D2342A6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D88AF2C8D5BFEB43F89E0BA2 /* 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 = "<group>"; };
F55339D7C431C33F2791F390 /* 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 = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FC9C4708640E3D61822E65FE /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
55400ED647B2A0FC3724CC7E /* Frameworks */ = {
isa = PBXGroup;
children = (
D559FAF83BE8A73D7D2342A6 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
86B90A2A5091E79163043380 /* Pods */ = {
isa = PBXGroup;
children = (
776376D62B8541C00FA61280 /* Pods-Runner.debug.xcconfig */,
F55339D7C431C33F2791F390 /* Pods-Runner.release.xcconfig */,
D88AF2C8D5BFEB43F89E0BA2 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
86B90A2A5091E79163043380 /* Pods */,
55400ED647B2A0FC3724CC7E /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
isa = PBXGroup;
children = (
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
3756EFFCE0B733FE0DCC8F8E /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
A3B708A5D380EE3A6BAC0D0D /* [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 = 1020;
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 */
3756EFFCE0B733FE0DCC8F8E /* [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;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
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";
};
A3B708A5D380EE3A6BAC0D0D /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${PODS_ROOT}/../Flutter/Flutter.framework",
"${BUILT_PRODUCTS_DIR}/photo_gallery/photo_gallery.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_gallery.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
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 = 8.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)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.morbit.photoGalleryExample;
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;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
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 = 8.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
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 = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
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)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.morbit.photoGalleryExample;
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)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.morbit.photoGalleryExample;
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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -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)
}
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -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.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPhotoLibraryUsageDescription</key>
<string></string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>photo_gallery_example</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

235
example/lib/main.dart Normal file
View File

@ -0,0 +1,235 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_gallery/photo_gallery.dart';
import 'package:transparent_image/transparent_image.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List _collections;
bool _loading = false;
@override
void initState() {
super.initState();
_loading = true;
initAsync();
}
Future<void> initAsync() async {
if (await _promptPermissionSetting()) {
List<Album> collections =
await PhotoGallery.listAlbums(mediumType: MediumType.image);
setState(() {
_collections = collections;
_loading = false;
});
}
setState(() {
_loading = false;
});
}
Future<bool> _promptPermissionSetting() async {
if (Platform.isIOS &&
await Permission.storage.request().isGranted &&
await Permission.photos.request().isGranted ||
Platform.isAndroid && await Permission.storage.request().isGranted) {
return true;
}
return false;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Photo gallery example'),
),
body: _loading
? Center(
child: CircularProgressIndicator(),
)
: LayoutBuilder(
builder: (context, constraints) {
double gridWidth = (constraints.maxWidth - 20) / 3;
double gridHeight = gridWidth + 33;
double ratio = gridWidth / gridHeight;
return Container(
padding: EdgeInsets.all(5),
child: GridView.count(
childAspectRatio: ratio,
crossAxisCount: 3,
mainAxisSpacing: 5.0,
crossAxisSpacing: 5.0,
children: <Widget>[
...?_collections?.map(
(collection) => GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
CollectionPage(collection))),
child: Column(
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: Container(
color: Colors.grey[300],
height: gridWidth,
width: gridWidth,
child: FadeInImage(
fit: BoxFit.cover,
placeholder:
MemoryImage(kTransparentImage),
image: AlbumThumbnailProvider(
albumId: collection.id,
highQuality: true,
),
),
),
),
Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.only(left: 2.0),
child: Text(
collection.name,
maxLines: 1,
textAlign: TextAlign.start,
style: TextStyle(
height: 1.2,
fontSize: 16,
),
),
),
Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.only(left: 2.0),
child: Text(
collection.count.toString(),
textAlign: TextAlign.start,
style: TextStyle(
height: 1.2,
fontSize: 12,
),
),
),
],
),
),
),
],
),
);
},
),
),
);
}
}
class CollectionPage extends StatefulWidget {
final Album collection;
CollectionPage(Album collection) : collection = collection;
@override
State<StatefulWidget> createState() => CollectionPageState();
}
class CollectionPageState extends State<CollectionPage> {
List<Medium> _media;
@override
void initState() {
super.initState();
initAsync();
}
void initAsync() async {
MediaPage mediaPage = await widget.collection.listMedia();
setState(() {
_media = mediaPage.items;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(widget.collection.name),
),
body: GridView.count(
crossAxisCount: 3,
mainAxisSpacing: 1.0,
crossAxisSpacing: 1.0,
children: <Widget>[
...?_media?.map(
(media) => GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ViewerPage(media))),
child: Container(
color: Colors.grey[300],
child: FadeInImage(
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: ThumbnailProvider(
mediumId: media.id,
highQuality: true,
),
),
),
),
),
],
),
),
);
}
}
class ViewerPage extends StatelessWidget {
final Medium media;
ViewerPage(Medium media) : media = media;
@override
Widget build(BuildContext context) {
DateTime date = media.creationDate ?? media.modifiedDate;
return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.arrow_back_ios),
),
title: Text(date?.toLocal().toString()),
),
body: Container(
alignment: Alignment.center,
child: FadeInImage(
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: PhotoProvider(mediumId: media.id),
),
),
),
);
}
}

224
example/pubspec.lock Normal file
View File

@ -0,0 +1,224 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.12"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.12"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.4"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.1+1"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
photo_gallery:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.1.0+1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.15"
transparent_image:
dependency: "direct main"
description:
name: transparent_image
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.1"
sdks:
dart: ">=2.7.0 <3.0.0"
flutter: ">=1.12.8 <2.0.0"

73
example/pubspec.yaml Normal file
View File

@ -0,0 +1,73 @@
name: photo_gallery_example
description: Demonstrates how to use the photo_gallery plugin.
# The following line prevents the package from being accidentally published to
# pub.dev using `pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
photo_gallery:
# When depending on this package from a real application you should use:
# photo_gallery: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3
transparent_image: ^1.0.0
permission_handler: ^5.0.1+1
dev_dependencies:
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/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.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/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.dev/custom-fonts/#from-packages

37
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
.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
/Flutter/flutter_export_environment.sh

0
ios/Assets/.gitkeep Normal file
View File

View File

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface PhotoGalleryPlugin : NSObject<FlutterPlugin>
@end

View File

@ -0,0 +1,15 @@
#import "PhotoGalleryPlugin.h"
#if __has_include(<photo_gallery/photo_gallery-Swift.h>)
#import <photo_gallery/photo_gallery-Swift.h>
#else
// Support project import fallback if the generated compatibility header
// is not copied when this plugin is created as a library.
// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
#import "photo_gallery-Swift.h"
#endif
@implementation PhotoGalleryPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftPhotoGalleryPlugin registerWithRegistrar:registrar];
}
@end

View File

@ -0,0 +1,440 @@
import Foundation
import MobileCoreServices
import Flutter
import UIKit
import Photos
public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "photo_gallery", binaryMessenger: registrar.messenger())
let instance = SwiftPhotoGalleryPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if(call.method == "listAlbums") {
let arguments = call.arguments as! Dictionary<String, AnyObject>
let mediumType = arguments["mediumType"] as! String
result(listAlbums(mediumType: mediumType))
}
else if(call.method == "listMedia") {
let arguments = call.arguments as! Dictionary<String, AnyObject>
let albumId = arguments["albumId"] as! String
let mediumType = arguments["mediumType"] as! String
let skip = arguments["skip"] as? NSNumber
let take = arguments["take"] as? NSNumber
result(listMedia(albumId: albumId, skip: skip, take: take, mediumType: mediumType))
}
else if(call.method == "getMedium") {
let arguments = call.arguments as! Dictionary<String, AnyObject>
let mediumId = arguments["mediumId"] as! String
getMedium(
mediumId: mediumId,
completion: { (data: [String: Any?]?, error: Error?) -> Void in
result(data)
})
}
else if(call.method == "getThumbnail") {
let arguments = call.arguments as! Dictionary<String, AnyObject>
let mediumId = arguments["mediumId"] as! String
let width = arguments["width"] as? NSNumber
let height = arguments["height"] as? NSNumber
let highQuality = arguments["highQuality"] as? Bool
getThumbnail(
mediumId: mediumId,
width: width,
height: height,
highQuality: highQuality,
completion: { (data: Data?, error: Error?) -> Void in
result(data)
})
}
else if(call.method == "getAlbumThumbnail") {
let arguments = call.arguments as! Dictionary<String, AnyObject>
let albumId = arguments["albumId"] as! String
let width = arguments["width"] as? Int
let height = arguments["height"] as? Int
let highQuality = arguments["highQuality"] as? Bool
getAlbumThumbnail(
albumId: albumId,
width: width,
height: height,
highQuality: highQuality,
completion: { (data: Data?, error: Error?) -> Void in
result(data)
})
}
else if(call.method == "getFile") {
let arguments = call.arguments as! Dictionary<String, AnyObject>
let mediumId = arguments["mediumId"] as! String
getFile(
mediumId: mediumId,
completion: { (filepath: String?, error: Error?) -> Void in
result(filepath?.replacingOccurrences(of: "file://", with: ""))
})
}
else {
result(FlutterMethodNotImplemented)
}
}
private var assetCollections : [PHAssetCollection] = []
private func listAlbums(mediumType: String) -> [NSDictionary] {
self.assetCollections = []
let fetchOptions = PHFetchOptions()
var total = 0
var albums = [NSDictionary]()
var albumIds = Set<String>()
func addCollection (collection: PHAssetCollection, hideIfEmpty: Bool) -> Void {
let kRecentlyDeletedCollectionSubtype = PHAssetCollectionSubtype(rawValue: 1000000201)
guard collection.assetCollectionSubtype != kRecentlyDeletedCollectionSubtype else { return }
// De-duplicate by id.
let albumId = collection.localIdentifier
guard !albumIds.contains(albumId) else { return }
albumIds.insert(albumId)
let options = PHFetchOptions()
options.predicate = self.predicateFromMediumType(mediumType: mediumType)
if #available(iOS 9, *) {
fetchOptions.fetchLimit = 1
}
let count = PHAsset.fetchAssets(in: collection, options: options).count
if(count > 0 || !hideIfEmpty) {
total+=count
self.assetCollections.append(collection)
albums.append([
"id": collection.localIdentifier,
"mediumType": mediumType,
"name": collection.localizedTitle ?? "Unknown",
"count": count,
])
}
}
func processPHAssetCollections(fetchResult: PHFetchResult<PHAssetCollection>, hideIfEmpty: Bool) -> Void {
fetchResult.enumerateObjects { (assetCollection, _, _) in
addCollection(collection: assetCollection, hideIfEmpty: hideIfEmpty)
}
}
func processPHCollections (fetchResult: PHFetchResult<PHCollection>, hideIfEmpty: Bool) -> Void {
fetchResult.enumerateObjects { (collection, _, _) in
if let assetCollection = collection as? PHAssetCollection {
addCollection(collection: assetCollection, hideIfEmpty: hideIfEmpty)
} else if let collectionList = collection as? PHCollectionList {
processPHCollections(fetchResult: PHCollectionList.fetchCollections(in: collectionList, options: nil), hideIfEmpty: hideIfEmpty)
}
}
}
// Smart Albums.
processPHAssetCollections(
fetchResult: PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: fetchOptions),
hideIfEmpty: true
)
// User-created collections.
processPHCollections(
fetchResult: PHAssetCollection.fetchTopLevelUserCollections(with: fetchOptions),
hideIfEmpty: true
)
albums.insert([
"id": "__ALL__",
"mediumType": mediumType,
"name": "All",
"count" : countMedia(collection: nil, mediumTypes: [mediumType]),
], at: 0)
return albums
}
private func countMedia(collection: PHAssetCollection?, mediumTypes: [String]) -> Int {
let options = PHFetchOptions()
options.predicate = self.predicateFromMediumTypes(mediumTypes: mediumTypes)
if(collection == nil) {
return PHAsset.fetchAssets(with: options).count
}
return PHAsset.fetchAssets(in: collection!, options: options).count
}
private func listMedia(albumId: String, skip: NSNumber?, take: NSNumber?, mediumType: String) -> NSDictionary {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchOptions.predicate = predicateFromMediumType(mediumType: mediumType)
let collection = self.assetCollections.first(where: { (collection) -> Bool in
collection.localIdentifier == albumId
})
let fetchResult = albumId == "__ALL__"
? PHAsset.fetchAssets(with: fetchOptions)
: PHAsset.fetchAssets(in: collection!, options: fetchOptions)
let start = skip?.intValue ?? 0
let total = fetchResult.count
let end = take == nil ? total : min(start + take!.intValue, total)
var items = [[String: Any?]]()
for index in start..<end {
let asset = fetchResult.object(at: index) as PHAsset
items.append(getMediumFromAsset(asset: asset))
}
return [
"start": start,
"total": total,
"items": items,
]
}
private func getMedium(mediumId: String, completion: @escaping ([String : Any?]?, Error?) -> Void) {
let fetchOptions = PHFetchOptions()
if #available(iOS 9, *) {
fetchOptions.fetchLimit = 1
}
let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions)
if (assets.count > 0) {
let asset: PHAsset = assets[0]
completion(getMediumFromAsset(asset: asset), nil)
return
}
completion(nil, NSError(domain: "photo_gallery", code: 404, userInfo: nil))
}
private func getThumbnail(
mediumId: String,
width: NSNumber?,
height: NSNumber?,
highQuality: Bool?,
completion: @escaping (Data?, Error?) -> Void
) {
let manager = PHImageManager.default()
let fetchOptions = PHFetchOptions()
if #available(iOS 9, *) {
fetchOptions.fetchLimit = 1
}
let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions)
if (assets.count > 0) {
let asset: PHAsset = assets[0]
let options = PHImageRequestOptions()
options.deliveryMode = (highQuality ?? false)
? PHImageRequestOptionsDeliveryMode.highQualityFormat
: PHImageRequestOptionsDeliveryMode.fastFormat
options.isSynchronous = false
options.isNetworkAccessAllowed = true
options.version = .current
let imageSize = CGSize(width: width?.intValue ?? 128, height: height?.intValue ?? 128)
manager.requestImage(
for: asset,
targetSize: CGSize(width: imageSize.width * UIScreen.main.scale, height: imageSize.height * UIScreen.main.scale),
contentMode: PHImageContentMode.aspectFill,
options: options,
resultHandler: {(uiImage: UIImage?, info) in
guard let image = uiImage else {
completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil))
return
}
let bytes = image.jpegData(compressionQuality: CGFloat(70))
completion(bytes, nil)
})
return
}
completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil))
}
private func getAlbumThumbnail(
albumId: String,
width: Int?,
height: Int?,
highQuality: Bool?,
completion: @escaping (Data?, Error?) -> Void
) {
let manager = PHImageManager.default()
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
if #available(iOS 9, *) {
fetchOptions.fetchLimit = 1
}
let assets = albumId == "__ALL__" ?
PHAsset.fetchAssets(with: fetchOptions) :
PHAsset.fetchAssets(in: self.assetCollections.first(where: { (collection) -> Bool in
collection.localIdentifier == albumId
})!, options: fetchOptions)
if (assets.count > 0) {
let asset: PHAsset = assets[0]
let options = PHImageRequestOptions()
options.deliveryMode = (highQuality ?? false)
? PHImageRequestOptionsDeliveryMode.highQualityFormat
: PHImageRequestOptionsDeliveryMode.fastFormat
options.isSynchronous = false
options.isNetworkAccessAllowed = true
options.version = .current
let imageSize = CGSize(width: width ?? 128, height: height ?? 128)
manager.requestImage(
for: asset,
targetSize: CGSize(
width: imageSize.width * UIScreen.main.scale,
height: imageSize.height * UIScreen.main.scale
),
contentMode: PHImageContentMode.aspectFill,
options: options,
resultHandler: {(uiImage: UIImage?, info) in
guard let image = uiImage else {
completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil))
return
}
let bytes = image.jpegData(compressionQuality: CGFloat(80))
completion(bytes, nil)
})
return
}
completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil))
}
private func getFile(mediumId: String, completion: @escaping (String?, Error?) -> Void) {
let manager = PHImageManager.default()
let fetchOptions = PHFetchOptions()
if #available(iOS 9, *) {
fetchOptions.fetchLimit = 1
}
let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions)
if (assets.count > 0) {
let asset: PHAsset = assets[0]
if(asset.mediaType == PHAssetMediaType.image) {
let options = PHImageRequestOptions()
options.deliveryMode = PHImageRequestOptionsDeliveryMode.highQualityFormat
options.isSynchronous = false
options.isNetworkAccessAllowed = true
options.version = .current
manager.requestImageData(
for: asset,
options: options,
resultHandler: { (data: Data?, uti: String?, orientation, info) in
DispatchQueue.main.async(execute: {
guard let originalData = data else {
completion(nil, NSError(domain: "photo_gallery", code: 404, userInfo: nil))
return
}
guard let jpgData = self.convertToJpeg(originalData: originalData) else {
completion(nil, NSError(domain: "photo_gallery", code: 500, userInfo: nil))
return
}
// Writing to file
let filepath = self.exportPathForAsset(asset: asset, ext: ".jpg")
try! jpgData.write(to: filepath, options: .atomic)
completion(filepath.absoluteString, nil)
})
})
} else if(asset.mediaType == PHAssetMediaType.video
|| asset.mediaType == PHAssetMediaType.audio) {
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
options.version = .current
manager.requestAVAsset(forVideo: asset, options: options, resultHandler: { (avAsset, avAudioMix, info) in
DispatchQueue.main.async(execute: {
do {
let avAsset = avAsset as? AVURLAsset
let data = try Data(contentsOf: avAsset!.url)
let filepath = self.exportPathForAsset(asset: asset, ext: ".mov")
try! data.write(to: filepath, options: .atomic)
completion(filepath.absoluteString, nil)
} catch {
completion(nil, NSError(domain: "photo_gallery", code: 500, userInfo: nil))
}
})
})
}
}
}
private func getMediumFromAsset(asset: PHAsset) -> [String: Any?] {
return [
"id": asset.localIdentifier,
"mediumType": toDartMediumType(value: asset.mediaType),
"height": asset.pixelHeight,
"width": asset.pixelWidth,
"creationDate": (asset.creationDate != nil) ? NSInteger(asset.creationDate!.timeIntervalSince1970) * 1000 : nil,
"modifiedDate": (asset.modificationDate != nil) ? NSInteger(asset.modificationDate!.timeIntervalSince1970) * 1000 : nil
]
}
/// Converts to JPEG, and keep EXIF data.
private func convertToJpeg(originalData: Data) -> Data? {
guard let image: UIImage = UIImage(data: originalData) else { return nil }
let originalSrc = CGImageSourceCreateWithData(originalData as CFData, nil)!
let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse]
let originalMetadata = CGImageSourceCopyPropertiesAtIndex(originalSrc, 0, options as CFDictionary)
guard let jpeg = image.jpegData(compressionQuality: 1.0) else { return nil }
let src = CGImageSourceCreateWithData(jpeg as CFData, nil)!
let data = NSMutableData()
let uti = CGImageSourceGetType(src)!
let dest = CGImageDestinationCreateWithData(data as CFMutableData, uti, 1, nil)!
CGImageDestinationAddImageFromSource(dest, src, 0, originalMetadata)
if !CGImageDestinationFinalize(dest) { return nil }
return data as Data
}
private func exportPathForAsset(asset: PHAsset, ext: String) -> URL {
let mediumId = asset.localIdentifier
.replacingOccurrences(of: "/", with: "__")
.replacingOccurrences(of: "\\", with: "__")
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let tempFolder = paths[0].appendingPathComponent("photo_gallery")
try! FileManager.default.createDirectory(at: tempFolder, withIntermediateDirectories: true, attributes: nil)
return paths[0].appendingPathComponent(mediumId+ext)
}
private func toSwiftMediumType(value: String) -> PHAssetMediaType? {
switch value {
case "image": return PHAssetMediaType.image
case "video": return PHAssetMediaType.video
case "audio": return PHAssetMediaType.audio
default: return nil
}
}
private func toDartMediumType(value: PHAssetMediaType) -> String? {
switch value {
case PHAssetMediaType.image: return "image"
case PHAssetMediaType.video: return "video"
case PHAssetMediaType.audio: return "audio"
default: return nil
}
}
private func predicateFromMediumTypes(mediumTypes: [String]) -> NSPredicate {
let predicates = mediumTypes.map { (dartValue) -> NSPredicate in
return predicateFromMediumType(mediumType: dartValue)
}
return NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.or, subpredicates: predicates)
}
private func predicateFromMediumType(mediumType: String) -> NSPredicate {
let swiftType = toSwiftMediumType(value: mediumType)
return NSPredicate(format: "mediaType = %d", swiftType!.rawValue)
}
}

23
ios/photo_gallery.podspec Normal file
View File

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint photo_gallery.podspec' to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'photo_gallery'
s.version = '0.0.1'
s.summary = 'A Flutter plugin that retrieves images and videos from mobile native gallery.'
s.description = <<-DESC
A Flutter plugin that retrieves images and videos from mobile native gallery.
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.dependency 'Flutter'
s.platform = :ios, '8.0'
# Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
s.swift_version = '5.0'
end

112
lib/photo_gallery.dart Normal file
View File

@ -0,0 +1,112 @@
library photogallery;
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:transparent_image/transparent_image.dart';
part 'src/common/medium_type.dart';
part 'src/image_providers/album_thumbnail_provider.dart';
part 'src/image_providers/photo_provider.dart';
part 'src/image_providers/thumbnail_provider.dart';
part 'src/models/album.dart';
part 'src/models/media_page.dart';
part 'src/models/medium.dart';
/// Accessing the native photo gallery.
class PhotoGallery {
static const MethodChannel _channel = const MethodChannel('photo_gallery');
/// List all available photo gallery albums and counts number of
/// items of [MediumType].
static Future<List<Album>> listAlbums({
@required MediumType mediumType,
}) async {
assert(mediumType != null);
final json = await _channel.invokeMethod('listAlbums', {
'mediumType': mediumTypeToJson(mediumType),
});
return json.map<Album>((x) => Album.fromJson(x)).toList();
}
static Future<MediaPage> _listMedia({
@required Album album,
@required MediumType mediumType,
@required int total,
int skip,
int take,
}) async {
assert(album.id != null);
final json = await _channel.invokeMethod('listMedia', {
'albumId': album.id,
'mediumType': mediumTypeToJson(mediumType),
'total': total,
'skip': skip,
'take': take,
});
return MediaPage.fromJson(album, mediumType, json);
}
static Future<Medium> getMedium({
@required String mediumId,
MediumType mediumType,
}) async {
assert(mediumId != null);
final json = await _channel.invokeMethod('getMedium', {
'mediumId': mediumId,
'mediumType': mediumTypeToJson(mediumType),
});
return Medium.fromJson(json);
}
static Future<List<dynamic>> getThumbnail({
@required String mediumId,
MediumType mediumType,
int width,
int height,
bool highQuality,
}) async {
assert(mediumId != null);
final bytes = await _channel.invokeMethod('getThumbnail', {
'mediumId': mediumId,
'mediumType': mediumTypeToJson(mediumType),
'width': width,
'height': height,
'highQuality': highQuality,
});
return bytes;
}
static Future<List<dynamic>> getAlbumThumbnail({
@required String albumId,
int width,
int height,
bool highQuality,
}) async {
assert(albumId != null);
final bytes = await _channel.invokeMethod('getAlbumThumbnail', {
'albumId': albumId,
'width': width,
'height': height,
'highQuality': highQuality,
});
return bytes;
}
static Future<File> getFile({
@required String mediumId,
MediumType mediumType,
}) async {
assert(mediumId != null);
final path = await _channel.invokeMethod('getFile', {
'mediumId': mediumId,
'mediumType': mediumTypeToJson(mediumType),
}) as String;
return File(path);
}
}

View File

@ -0,0 +1,29 @@
part of photogallery;
/// A medium type.
enum MediumType {
image,
video,
}
String mediumTypeToJson(MediumType value) {
switch (value) {
case MediumType.image:
return 'image';
case MediumType.video:
return 'video';
default:
return null;
}
}
MediumType jsonToMediumType(String value) {
switch (value) {
case 'image':
return MediumType.image;
case 'video':
return MediumType.video;
default:
return null;
}
}

View File

@ -0,0 +1,60 @@
part of photogallery;
/// Fetches the given album thumbnail from the gallery.
class AlbumThumbnailProvider extends ImageProvider<AlbumThumbnailProvider> {
const AlbumThumbnailProvider({
@required this.albumId,
this.height,
this.width,
this.highQuality = false,
}) : assert(albumId != null);
final String albumId;
final int height;
final int width;
final bool highQuality;
@override
ImageStreamCompleter load(key, decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
informationCollector: () sync* {
yield ErrorDescription('Id: $albumId');
},
);
}
Future<ui.Codec> _loadAsync(
AlbumThumbnailProvider key, DecoderCallback decode) async {
assert(key == this);
final bytes = await PhotoGallery.getAlbumThumbnail(
albumId: albumId,
height: height,
width: width,
highQuality: highQuality,
);
if (bytes == null || bytes.length == 0)
return await decode(kTransparentImage);
return await decode(bytes);
}
@override
Future<AlbumThumbnailProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<AlbumThumbnailProvider>(this);
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final AlbumThumbnailProvider typedOther = other;
return albumId == typedOther.albumId;
}
@override
int get hashCode => albumId?.hashCode ?? 0;
@override
String toString() => '$runtimeType("$albumId")';
}

View File

@ -0,0 +1,51 @@
part of photogallery;
/// Fetches the given image from the gallery.
class PhotoProvider extends ImageProvider<PhotoProvider> {
PhotoProvider({
@required this.mediumId,
}) : assert(mediumId != null);
final String mediumId;
@override
ImageStreamCompleter load(key, decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
informationCollector: () sync* {
yield ErrorDescription('Id: $mediumId');
},
);
}
Future<ui.Codec> _loadAsync(PhotoProvider key, DecoderCallback decode) async {
assert(key == this);
final file = await PhotoGallery.getFile(
mediumId: mediumId, mediumType: MediumType.image);
if (file == null) return null;
final bytes = await file.readAsBytes();
if (bytes.lengthInBytes == 0) return null;
return await decode(bytes);
}
@override
Future<PhotoProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<PhotoProvider>(this);
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final PhotoProvider typedOther = other;
return mediumId == typedOther.mediumId;
}
@override
int get hashCode => mediumId?.hashCode ?? 0;
@override
String toString() => '$runtimeType("$mediumId")';
}

View File

@ -0,0 +1,59 @@
part of photogallery;
/// Fetches the given medium thumbnail from the gallery.
class ThumbnailProvider extends ImageProvider<ThumbnailProvider> {
const ThumbnailProvider({
@required this.mediumId,
this.height,
this.width,
this.highQuality,
}) : assert(mediumId != null);
final String mediumId;
final int height;
final int width;
final bool highQuality;
@override
ImageStreamCompleter load(key, decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
informationCollector: () sync* {
yield ErrorDescription('Id: $mediumId');
},
);
}
Future<ui.Codec> _loadAsync(
ThumbnailProvider key, DecoderCallback decode) async {
assert(key == this);
final bytes = await PhotoGallery.getThumbnail(
mediumId: mediumId,
height: height,
width: width,
highQuality: highQuality,
);
if (bytes.length == 0) return null;
return await decode(bytes);
}
@override
Future<ThumbnailProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<ThumbnailProvider>(this);
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final ThumbnailProvider typedOther = other;
return mediumId == typedOther.mediumId;
}
@override
int get hashCode => mediumId?.hashCode ?? 0;
@override
String toString() => '$runtimeType("$mediumId")';
}

82
lib/src/models/album.dart Normal file
View File

@ -0,0 +1,82 @@
part of photogallery;
/// A album in the gallery.
@immutable
class Album {
/// A unique identifier for the album.
final String id;
/// The [MediumType] of the album.
final MediumType mediumType;
/// The name of the album.
final String name;
/// The total number of media in the album.
final int count;
/// Indicates whether this album contains all media.
bool get isAllAlbum => id == "__ALL__";
/// Creates a album from platform channel protocol.
Album.fromJson(dynamic json)
: id = json['id'],
mediumType = jsonToMediumType(json['mediumType']),
name = json['name'],
count = json['count'];
/// list media in the album.
///
/// Pagination can be controlled out of [skip] (defaults to `0`) and
/// [take] (defaults to `<total>`).
Future<MediaPage> listMedia({
int skip,
int take,
}) {
return PhotoGallery._listMedia(
album: this,
mediumType: this.mediumType,
total: this.count,
skip: skip,
take: take,
);
}
/// Get thumbnail data for this album.
///
/// It will display the lastly taken medium thumbnail.
Future<List<int>> getThumbnail({
int width,
int height,
bool highQuality = false,
}) {
return PhotoGallery.getAlbumThumbnail(
albumId: id,
width: width,
height: height,
highQuality: highQuality,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Album &&
runtimeType == other.runtimeType &&
id == other.id &&
mediumType == other.mediumType &&
name == other.name &&
count == other.count;
@override
int get hashCode =>
id.hashCode ^ mediumType.hashCode ^ name.hashCode ^ count.hashCode;
@override
String toString() {
return 'Album{id: $id, '
'mediumType: $mediumType, '
'name: $name, '
'count: $count}';
}
}

View File

@ -0,0 +1,71 @@
part of photogallery;
/// A list of media with pagination support.
@immutable
class MediaPage {
final Album album;
/// The medium type of [items].
final MediumType mediumType;
/// The start offset for those media.
final int start;
/// The total number of items.
final int total;
/// The current items.
final List<Medium> items;
/// The end index in the album.
int get end => start + items.length;
///Indicates whether this page is the last in the album.
bool get isLast => end >= total;
/// Creates a range of media from platform channel protocol.
MediaPage.fromJson(this.album, this.mediumType, dynamic json)
: start = json['start'],
total = json['total'],
items = json['items'].map<Medium>((x) => Medium.fromJson(x)).toList();
/// Gets the next page of media in the album.
Future<MediaPage> nextPage() {
assert(!isLast);
return PhotoGallery._listMedia(
album: album,
mediumType: mediumType,
total: total,
skip: end,
take: items.length,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MediaPage &&
runtimeType == other.runtimeType &&
album == other.album &&
mediumType == other.mediumType &&
start == other.start &&
total == other.total &&
listEquals(items, other.items);
@override
int get hashCode =>
album.hashCode ^
mediumType.hashCode ^
start.hashCode ^
total.hashCode ^
items.hashCode;
@override
String toString() {
return 'MediaPage{album: $album, '
'mediumType: $mediumType, '
'start: $start, '
'total: $total, '
'items: $items}';
}
}

123
lib/src/models/medium.dart Normal file
View File

@ -0,0 +1,123 @@
part of photogallery;
/// A medium in the gallery.
///
/// It can be of image or video [mediumType].
@immutable
class Medium {
/// A unique identifier for the medium.
final String id;
/// The medium type.
final MediumType mediumType;
/// The medium width.
final int width;
/// The medium height.
final int height;
/// The date at which the photo or video was taken.
final DateTime creationDate;
/// The date at which the photo or video was modified.
final DateTime modifiedDate;
Medium({
this.id,
this.mediumType,
this.width,
this.height,
this.creationDate,
this.modifiedDate,
});
/// Creates a medium from platform channel protocol.
Medium.fromJson(dynamic json)
: id = json["id"],
mediumType = jsonToMediumType(json["mediumType"]),
width = json["width"],
height = json["height"],
creationDate = json['creationDate'] != null
? DateTime.fromMillisecondsSinceEpoch(json['creationDate'])
: null,
modifiedDate = json['modifiedDate'] != null
? DateTime.fromMillisecondsSinceEpoch(json['modifiedDate'])
: null;
static Medium fromMap(Map map) {
return Medium(
id: map['id'],
mediumType: jsonToMediumType(map['mediumType']),
width: map['width'],
height: map['height'],
creationDate: map['creationDate'],
modifiedDate: map['modifiedDate'],
);
}
Map toMap() {
return {
"id": this.id,
"mediumType": mediumTypeToJson(this.mediumType),
"height": this.height,
"width": this.width,
"creationDate": this.creationDate,
"modifiedDate": this.modifiedDate,
};
}
/// Get a JPEG thumbnail's data for this medium.
Future<List<int>> getThumbnail({
int width,
int height,
bool highQuality = false,
}) {
return PhotoGallery.getThumbnail(
mediumId: id,
width: width,
height: height,
mediumType: mediumType,
highQuality: highQuality,
);
}
/// Get the original file.
Future<File> getFile() {
return PhotoGallery.getFile(
mediumId: id,
mediumType: mediumType,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Medium &&
runtimeType == other.runtimeType &&
id == other.id &&
mediumType == other.mediumType &&
width == other.width &&
height == other.height &&
creationDate == other.creationDate &&
modifiedDate == other.modifiedDate;
@override
int get hashCode =>
id.hashCode ^
mediumType.hashCode ^
width.hashCode ^
height.hashCode ^
creationDate.hashCode ^
modifiedDate.hashCode;
@override
String toString() {
return 'Medium{id: $id, '
'mediumType: $mediumType, '
'width: $width, '
'height: $height, '
'creationDate: $creationDate, '
'modifiedDate: $modifiedDate}';
}
}

189
pubspec.lock Normal file
View File

@ -0,0 +1,189 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.12"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.12"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.4"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.15"
transparent_image:
dependency: "direct main"
description:
name: transparent_image
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.1"
sdks:
dart: ">=2.7.0 <3.0.0"
flutter: ">=1.10.0"

65
pubspec.yaml Normal file
View File

@ -0,0 +1,65 @@
name: photo_gallery
description: A Flutter plugin that retrieves images and videos from mobile native gallery.
version: 0.1.0+1
repository: https://github.com/Firelands128/photo_gallery
environment:
sdk: ">=2.7.0 <3.0.0"
flutter: ">=1.10.0"
dependencies:
flutter:
sdk: flutter
transparent_image: ^1.0.0
dev_dependencies:
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' and Android 'package' identifiers should not ordinarily
# be modified. They are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.morbit.photogallery
pluginClass: PhotoGalleryPlugin
ios:
pluginClass: PhotoGalleryPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# To add custom fonts to your plugin package, 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 in packages, see
# https://flutter.dev/custom-fonts/#from-packages

View File

@ -0,0 +1,87 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:photo_gallery/photo_gallery.dart';
import 'utils/generator.dart';
import 'utils/mock_handler.dart';
void main() {
const MethodChannel channel = MethodChannel('photo_gallery');
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
channel.setMockMethodCallHandler(mockMethodCallHandler);
});
tearDown(() {
channel.setMockMethodCallHandler(null);
});
test('list albums', () async {
MediumType mediumType = MediumType.image;
var result = await PhotoGallery.listAlbums(mediumType: mediumType);
var expected = Generator.generateCollections(mediumType: mediumType);
expect(result, expected);
});
test('list media', () async {
MediumType mediumType = MediumType.image;
int skip = 0;
int take = 1;
List<Album> collections =
await PhotoGallery.listAlbums(mediumType: mediumType);
Album allCollection =
collections.firstWhere((element) => element.isAllAlbum);
MediaPage result = await allCollection.listMedia(skip: skip, take: take);
MediaPage expected = Generator.generateMediaPage(
collection: allCollection,
mediumType: mediumType,
skip: skip,
take: take,
);
expect(result, expected);
});
test('get medium', () async {
String mediumId = 0.toString();
MediumType mediumType = MediumType.image;
Medium result = await PhotoGallery.getMedium(
mediumId: mediumId,
mediumType: mediumType,
);
Medium expected =
Generator.generateMedia(mediumId: mediumId, mediumType: mediumType);
expect(result, expected);
});
test('get thumbnail', () async {
String mediumId = 0.toString();
MediumType mediumType = MediumType.image;
List result = await PhotoGallery.getThumbnail(
mediumId: mediumId, mediumType: mediumType);
List expected = Generator.generateMockThumbnail(
mediumId: mediumId, mediumType: mediumType);
expect(result, expected);
});
test('get collection thumbnail', () async {
String collectionId = "__ALL__";
List result = await PhotoGallery.getAlbumThumbnail(albumId: collectionId);
List expected =
Generator.generateMockCollectionThumbnail(collectionId: collectionId);
expect(result, expected);
});
test('get file', () async {
String mediumId = 0.toString();
MediumType mediumType = MediumType.image;
File result =
await PhotoGallery.getFile(mediumId: mediumId, mediumType: mediumType);
File expected =
Generator.generateFile(mediumId: mediumId, mediumType: mediumType);
expect(result.path, expected.path);
});
}

119
test/utils/generator.dart Normal file
View File

@ -0,0 +1,119 @@
import 'dart:io';
import 'package:photo_gallery/photo_gallery.dart';
class Generator {
static dynamic generateCollectionsJson({MediumType mediumType}) {
mediumType = mediumType ?? MediumType.image;
return [
{
"id": "__ALL__",
"mediumType": mediumTypeToJson(mediumType),
"name": "All",
"count": 5,
},
{
"id": "CollectionId",
"mediumType": mediumTypeToJson(mediumType),
"name": "CollectionName",
"count": 5,
}
];
}
static List<Album> generateCollections({MediumType mediumType}) {
return Generator.generateCollectionsJson(mediumType: mediumType)
.map<Album>((x) => Album.fromJson(x))
.toList();
}
static dynamic generateMediaPageJson({
String collectionId,
MediumType mediumType,
int total,
int skip,
int take,
}) {
skip = skip ?? 0;
take = take ?? (total - skip);
var items = [];
int index = skip;
while (index < skip + take) {
items.add(generateMediaJson(
mediumId: index.toString(), mediumType: mediumType));
index++;
}
return {
"start": skip,
"total": total,
"items": items,
};
}
static dynamic generateMediaJson({
String mediumId,
MediumType mediumType,
}) {
return {
"id": mediumId,
"mediumType": mediumTypeToJson(mediumType),
"width": 512,
"height": 512,
"creationDate": DateTime(2020, 8, 1).millisecondsSinceEpoch,
};
}
static MediaPage generateMediaPage({
Album collection,
MediumType mediumType,
int skip,
int take,
}) {
dynamic json = generateMediaPageJson(
collectionId: collection.id,
mediumType: mediumType,
total: collection.count,
skip: skip,
take: take,
);
return MediaPage.fromJson(collection, mediumType, json);
}
static Medium generateMedia({
String mediumId,
MediumType mediumType,
}) {
return Medium.fromJson(
generateMediaJson(mediumId: mediumId, mediumType: mediumType),
);
}
static List<int> generateMockThumbnail({
String mediumId,
MediumType mediumType,
}) {
return [1, 2, 3, 4, 5, 6, 7, 8, 9];
}
static List<int> generateMockCollectionThumbnail({
String collectionId,
}) {
return [1, 2, 3, 4, 5, 6, 7, 8, 9];
}
static String generateFilePath({
String mediumId,
MediumType mediumType,
}) {
return "/path/to/file";
}
static File generateFile({
String mediumId,
MediumType mediumType,
}) {
return File(generateFilePath(mediumId: mediumId, mediumType: mediumType));
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:photo_gallery/photo_gallery.dart';
import 'generator.dart';
Future<dynamic> mockMethodCallHandler(MethodCall call) async {
if (call.method == "listAlbums") {
MediumType mediumType = jsonToMediumType(call.arguments['mediumType']);
dynamic collections =
Generator.generateCollectionsJson(mediumType: mediumType);
return collections;
} else if (call.method == "listMedia") {
String collectionId = call.arguments['collectionId'];
MediumType mediumType = jsonToMediumType(call.arguments['mediumType']);
int total = call.arguments['total'];
int skip = call.arguments['skip'];
int take = call.arguments['take'];
dynamic mediaPage = Generator.generateMediaPageJson(
collectionId: collectionId,
mediumType: mediumType,
total: total,
skip: skip,
take: take,
);
return mediaPage;
} else if (call.method == "getMedium") {
String mediumId = call.arguments['mediumId'];
MediumType mediumType = jsonToMediumType(call.arguments['mediumType']);
dynamic media =
Generator.generateMediaJson(mediumId: mediumId, mediumType: mediumType);
return media;
} else if (call.method == "getThumbnail") {
String mediumId = call.arguments['mediumId'];
MediumType mediumType = jsonToMediumType(call.arguments['mediumType']);
dynamic thumbnail = Generator.generateMockThumbnail(
mediumId: mediumId, mediumType: mediumType);
return thumbnail;
} else if (call.method == "getAlbumThumbnail") {
String collectionId = call.arguments['collectionId'];
dynamic thumbnail =
Generator.generateMockCollectionThumbnail(collectionId: collectionId);
return thumbnail;
} else if (call.method == "getFile") {
String mediumId = call.arguments['mediumId'];
MediumType mediumType = jsonToMediumType(call.arguments['mediumType']);
dynamic path =
Generator.generateFilePath(mediumId: mediumId, mediumType: mediumType);
return path;
}
throw UnimplementedError();
}