diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c397902 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +dist/ +node_modules/ + + +Pods +Build +*.iml +.idea \ No newline at end of file diff --git a/CapacitorMedia.podspec b/CapacitorMedia.podspec new file mode 100644 index 0000000..7fde64c --- /dev/null +++ b/CapacitorMedia.podspec @@ -0,0 +1,13 @@ + + Pod::Spec.new do |s| + s.name = 'CapacitorMedia' + s.version = '0.0.1' + s.summary = 'Enable some media features for Capacitor, such as create albums, save videos and gifs.' + s.license = 'MIT' + s.homepage = 'https://github.com/stewwan/capacitor-media' + s.author = 'Stewan Silva' + s.source = { :git => 'https://github.com/stewwan/capacitor-media', :tag => s.version.to_s } + s.source_files = 'ios/Plugin/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' + s.ios.deployment_target = '11.0' + s.dependency 'Capacitor' + end \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ece4153 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# capacitor-media + +Capacitor plugin to activate media features such as saving videos and gifs into user's photo gallery + +## API + +- savePhoto +- saveVideo +- saveGif +- createAlbum +- getAlbums +- getMedias `only ios for now` + +## Usage + +```js +import { Media } from "capacitor-media"; +const media = new Media(); + +// +// Save video to a specfic album +media + .saveVideo({ path: "/path/to/the/file", album: "My Album" }) + .then(console.log) + .catch(console.log); + +// +// Get a list of user albums +media + .getAlbums() + .then(console.log) // -> { albums: [{name:'My Album'}, {name:'My Another Album'}]} + .catch(console.log); +``` + +## iOS setup + +- `ionic start my-cap-app --capacitor` +- `cd my-cap-app` +- `npm install —-save capacitor-media` +- `mkdir www && touch www/index.html` +- `npx cap add ios` +- `npx cap open ios` +- sign your app at xcode (general tab) + +> Tip: every time you change a native code you may need to clean up the cache (Product > Clean build folder) and then run the app again. + +## Android setup + +- `ionic start my-cap-app --capacitor` +- `cd my-cap-app` +- `npm install —-save capacitor-media` +- `mkdir www && touch www/index.html` +- `npx cap add android` +- `npx cap open android` +- `[extra step]` in android case we need to tell Capacitor to initialise the plugin: + +> on your `MainActivity.java` file add `import io.stewan.capacitor.media.MediaPlugin;` and then inside the init callback `add(MediaPlugin.class);` + +Now you should be set to go. Try to run your client using `ionic cap run android --livereload`. + +> Tip: every time you change a native code you may need to clean up the cache (Build > Clean Project | Build > Rebuild Project) and then run the app again. + +## Sample app + +(coming soon) + +## You may also like + +- [capacitor-intercom](https://github.com/stewwan/capacitor-intercom) +- [capacitor-fcm](https://github.com/stewwan/capacitor-fcm) +- [capacitor-twitter](https://github.com/stewwan/capacitor-twitter) + +Cheers 🍻 + +Follow me [@Twitter](https://twitter.com/StewanSilva) + +## License + +MIT diff --git a/android/.npmignore b/android/.npmignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/android/.npmignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/android/capacitor-media/.npmignore b/android/capacitor-media/.npmignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/android/capacitor-media/.npmignore @@ -0,0 +1 @@ +/build diff --git a/android/capacitor-media/build.gradle b/android/capacitor-media/build.gradle new file mode 100644 index 0000000..8fdb2c1 --- /dev/null +++ b/android/capacitor-media/build.gradle @@ -0,0 +1,50 @@ +buildscript { + repositories { + jcenter() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.1' + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 27 + defaultConfig { + minSdkVersion 21 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } +} + +repositories { + google() + jcenter() + mavenCentral() + maven { + url "https://dl.bintray.com/ionic-team/capacitor" + } +} + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'ionic-team:capacitor-android:1+' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.1' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' +} + diff --git a/android/capacitor-media/proguard-rules.pro b/android/capacitor-media/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/capacitor-media/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/capacitor-media/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java b/android/capacitor-media/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java new file mode 100644 index 0000000..1d58c77 --- /dev/null +++ b/android/capacitor-media/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.android; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.getcapacitor.android", appContext.getPackageName()); + } +} diff --git a/android/capacitor-media/src/main/AndroidManifest.xml b/android/capacitor-media/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5568c4 --- /dev/null +++ b/android/capacitor-media/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/android/capacitor-media/src/main/java/io/stewan/capacitor/media/MediaPlugin.java b/android/capacitor-media/src/main/java/io/stewan/capacitor/media/MediaPlugin.java new file mode 100644 index 0000000..29c7889 --- /dev/null +++ b/android/capacitor-media/src/main/java/io/stewan/capacitor/media/MediaPlugin.java @@ -0,0 +1,290 @@ +package io.stewan.capacitor.media; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; +import android.provider.MediaStore; + +import com.getcapacitor.JSArray; +import com.getcapacitor.JSObject; +import com.getcapacitor.NativePlugin; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.text.SimpleDateFormat; +import java.util.Date; + + +@NativePlugin(permissions = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE +}) +public class MediaPlugin extends Plugin { + + // @todo + @PluginMethod() + public void getMedias(PluginCall call) { + call.unimplemented(); + } + + @PluginMethod() + public void getAlbums(PluginCall call) { + Log.d("DEBUG LOG", "GET ALBUMS"); + if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + Log.d("DEBUG LOG", "HAS PERMISSIONS"); + _getAlbums(call); + } else { + Log.d("DEBUG LOG", "NOT ALLOWED"); + saveCall(call); + pluginRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, 1986); + } + } + + private void _getAlbums(PluginCall call) { + Log.d("DEBUG LOG", "___GET ALBUMS"); + + JSObject response = new JSObject(); + JSArray albums = new JSArray(); + StringBuffer list = new StringBuffer(); + + String[] projection = new String[]{"DISTINCT " + MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}; + Cursor cur = getActivity().getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, null); + + while (cur.moveToNext()) { + String albumName = cur.getString((cur.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME))); + JSObject album = new JSObject(); + + list.append(albumName + "\n"); + + album.put("name", albumName); + albums.put(album); + } + + response.put("albums", albums); + Log.d("DEBUG LOG", String.valueOf(response)); + Log.d("DEBUG LOG", "___GET ALBUMS FINISHED"); + + call.resolve(response); + } + + + @PluginMethod() + public void getPhotos(PluginCall call) { + call.unimplemented(); + } + + @PluginMethod() + public void createAlbum(PluginCall call) { + Log.d("DEBUG LOG", "CREATE ALBUM"); + if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Log.d("DEBUG LOG", "HAS PERMISSIONS"); + _createAlbum(call); + } else { + Log.d("DEBUG LOG", "NOT ALLOWED"); + saveCall(call); + pluginRequestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, 1986); + } + } + + private void _createAlbum(PluginCall call) { + Log.d("DEBUG LOG", "___CREATE ALBUM"); + String folderName = call.getString("name"); + String folder = Environment.getExternalStorageDirectory() + "/" + folderName; + + File f = new File(folder); + + if (!f.exists()) { + if (!f.mkdir()) { + Log.d("DEBUG LOG", "___ERROR ALBUM"); + call.error("Cant create album"); + } else { + Log.d("DEBUG LOG", "___SUCCESS ALBUM CREATED"); + call.success(); + } + } else { + Log.d("DEBUG LOG", "___ERROR ALBUM ALREADY EXISTS"); + call.error("Album already exists"); + } + + } + + + @PluginMethod() + public void savePhoto(PluginCall call) { + Log.d("DEBUG LOG", "SAVE VIDEO TO ALBUM"); + if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Log.d("DEBUG LOG", "HAS PERMISSIONS"); + _saveMedia(call, "PICTURES"); + } else { + Log.d("DEBUG LOG", "NOT ALLOWED"); + saveCall(call); + pluginRequestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, 1986); + } + } + + @PluginMethod() + public void saveVideo(PluginCall call) { + Log.d("DEBUG LOG", "SAVE VIDEO TO ALBUM"); + if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Log.d("DEBUG LOG", "HAS PERMISSIONS"); + _saveMedia(call, "MOVIES"); + } else { + Log.d("DEBUG LOG", "NOT ALLOWED"); + saveCall(call); + pluginRequestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, 1986); + } + } + + + @PluginMethod() + public void saveGif(PluginCall call) { + Log.d("DEBUG LOG", "SAVE GIF TO ALBUM"); + if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Log.d("DEBUG LOG", "HAS PERMISSIONS"); + _saveMedia(call, "PICTURES"); + } else { + Log.d("DEBUG LOG", "NOT ALLOWED"); + saveCall(call); + pluginRequestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, 1986); + } + } + + + private void _saveMedia(PluginCall call, String destination) { + String dest; + if (destination == "MOVIES") { + dest = Environment.DIRECTORY_MOVIES; + } else { + dest = Environment.DIRECTORY_PICTURES; + } + + Log.d("DEBUG LOG", "___SAVE MEDIA TO ALBUM"); + String inputPath = call.getString("path"); + if (inputPath == null) { + call.reject("Input file path is required"); + return; + } + + Uri inputUri = Uri.parse(inputPath); + File inputFile = new File(inputUri.getPath()); + + String album = call.getString("album"); + File albumDir = Environment.getExternalStoragePublicDirectory(dest); + if (album != null) { + albumDir = new File(albumDir, album); + } + + try { + File expFile = copyFile(inputFile, albumDir); + scanPhoto(expFile); + + JSObject result = new JSObject(); + result.put("filePath", expFile.toString()); + call.resolve(result); + + } catch (RuntimeException e) { + call.reject("RuntimeException occurred", e); + } + + } + + private File copyFile(File inputFile, File albumDir) { + + // if destination folder does not exist, create it + if (!albumDir.exists()) { + if (!albumDir.mkdir()) { + throw new RuntimeException("Destination folder does not exist and cannot be created."); + } + } + + String absolutePath = inputFile.getAbsolutePath(); + String extension = absolutePath.substring(absolutePath.lastIndexOf(".")); + + // generate image file name using current date and time + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmssSSS").format(new Date()); + File newFile = new File(albumDir, "IMG_" + timeStamp + "." + extension); + + // Read and write image files + FileChannel inChannel = null; + FileChannel outChannel = null; + + try { + inChannel = new FileInputStream(inputFile).getChannel(); + } catch (FileNotFoundException e) { + throw new RuntimeException("Source file not found: " + inputFile + ", error: " + e.getMessage()); + } + try { + outChannel = new FileOutputStream(newFile).getChannel(); + } catch (FileNotFoundException e) { + throw new RuntimeException("Copy file not found: " + newFile + ", error: " + e.getMessage()); + } + + try { + inChannel.transferTo(0, inChannel.size(), outChannel); + } catch (IOException e) { + throw new RuntimeException("Error transfering file, error: " + e.getMessage()); + } finally { + if (inChannel != null) { + try { + inChannel.close(); + } catch (IOException e) { + Log.d("SaveImage", "Error closing input file channel: " + e.getMessage()); + // does not harm, do nothing + } + } + if (outChannel != null) { + try { + outChannel.close(); + } catch (IOException e) { + Log.d("SaveImage", "Error closing output file channel: " + e.getMessage()); + // does not harm, do nothing + } + } + } + + return newFile; + } + + private void scanPhoto(File imageFile) { + Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + Uri contentUri = Uri.fromFile(imageFile); + mediaScanIntent.setData(contentUri); + bridge.getActivity().sendBroadcast(mediaScanIntent); + } + + + @Override + protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.handleRequestPermissionsResult(requestCode, permissions, grantResults); + + if (savedLastCall == null) { + Log.d(getLogTag(), "No stored plugin call for permissions request result"); + return; + } + + for (int r : grantResults) { + if (r == PackageManager.PERMISSION_DENIED) { + Log.d(getLogTag(), "Permission not granted by the user"); + savedLastCall.reject("Permission denied"); + return; + } + } + + if (requestCode == 9800) { + // doWhatever(savedLastCall); + } + + savedLastCall = null; + } +} \ No newline at end of file diff --git a/android/capacitor-media/src/main/res/layout/bridge_layout_main.xml b/android/capacitor-media/src/main/res/layout/bridge_layout_main.xml new file mode 100644 index 0000000..b69e589 --- /dev/null +++ b/android/capacitor-media/src/main/res/layout/bridge_layout_main.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/android/capacitor-media/src/main/res/values/colors.xml b/android/capacitor-media/src/main/res/values/colors.xml new file mode 100644 index 0000000..045e125 --- /dev/null +++ b/android/capacitor-media/src/main/res/values/colors.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/capacitor-media/src/main/res/values/strings.xml b/android/capacitor-media/src/main/res/values/strings.xml new file mode 100644 index 0000000..6789e13 --- /dev/null +++ b/android/capacitor-media/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Just a simple string + diff --git a/android/capacitor-media/src/main/res/values/styles.xml b/android/capacitor-media/src/main/res/values/styles.xml new file mode 100644 index 0000000..f11f745 --- /dev/null +++ b/android/capacitor-media/src/main/res/values/styles.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/capacitor-media/src/test/java/com/getcapacitor/ExampleUnitTest.java b/android/capacitor-media/src/test/java/com/getcapacitor/ExampleUnitTest.java new file mode 100644 index 0000000..06806a7 --- /dev/null +++ b/android/capacitor-media/src/test/java/com/getcapacitor/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.getcapacitor; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0cfd5f2 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Dec 01 12:41:00 CST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/android/gradlew @@ -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 "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/android/gradlew.bat @@ -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 diff --git a/android/scripts/release.sh b/android/scripts/release.sh new file mode 100644 index 0000000..e69de29 diff --git a/ios/Plugin/Plugin.xcodeproj/project.pbxproj b/ios/Plugin/Plugin.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4d13666 --- /dev/null +++ b/ios/Plugin/Plugin.xcodeproj/project.pbxproj @@ -0,0 +1,587 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */; }; + 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */; }; + 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFF88201F53D600D50D53 /* Plugin.framework */; }; + 50ADFF97201F53D600D50D53 /* PluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFF96201F53D600D50D53 /* PluginTests.swift */; }; + 50ADFF99201F53D600D50D53 /* Plugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 50ADFF8B201F53D600D50D53 /* Plugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFFA52020D75100D50D53 /* Capacitor.framework */; }; + 50ADFFA82020EE4F00D50D53 /* Plugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFFA72020EE4F00D50D53 /* Plugin.m */; }; + 50E1A94820377CB70090CE1A /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1A94720377CB70090CE1A /* Plugin.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 50ADFF7F201F53D600D50D53 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 50ADFF87201F53D600D50D53; + remoteInfo = Plugin; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF88201F53D600D50D53 /* Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF8B201F53D600D50D53 /* Plugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Plugin.h; sourceTree = ""; }; + 50ADFF8C201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50ADFF91201F53D600D50D53 /* PluginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PluginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF96201F53D600D50D53 /* PluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTests.swift; sourceTree = ""; }; + 50ADFF98201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50ADFFA52020D75100D50D53 /* Capacitor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Capacitor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFFA72020EE4F00D50D53 /* Plugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Plugin.m; sourceTree = ""; }; + 50E1A94720377CB70090CE1A /* Plugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = ""; }; + 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig"; sourceTree = ""; }; + 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = ""; }; + 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = ""; }; + F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = ""; }; + F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PluginTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 50ADFF84201F53D600D50D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */, + 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8E201F53D600D50D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */, + 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 50ADFF7E201F53D600D50D53 = { + isa = PBXGroup; + children = ( + 50ADFF8A201F53D600D50D53 /* Plugin */, + 50ADFF95201F53D600D50D53 /* PluginTests */, + 50ADFF89201F53D600D50D53 /* Products */, + 8C8E7744173064A9F6D438E3 /* Pods */, + A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */, + ); + sourceTree = ""; + }; + 50ADFF89201F53D600D50D53 /* Products */ = { + isa = PBXGroup; + children = ( + 50ADFF88201F53D600D50D53 /* Plugin.framework */, + 50ADFF91201F53D600D50D53 /* PluginTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 50ADFF8A201F53D600D50D53 /* Plugin */ = { + isa = PBXGroup; + children = ( + 50E1A94720377CB70090CE1A /* Plugin.swift */, + 50ADFF8B201F53D600D50D53 /* Plugin.h */, + 50ADFFA72020EE4F00D50D53 /* Plugin.m */, + 50ADFF8C201F53D600D50D53 /* Info.plist */, + ); + path = Plugin; + sourceTree = ""; + }; + 50ADFF95201F53D600D50D53 /* PluginTests */ = { + isa = PBXGroup; + children = ( + 50ADFF96201F53D600D50D53 /* PluginTests.swift */, + 50ADFF98201F53D600D50D53 /* Info.plist */, + ); + path = PluginTests; + sourceTree = ""; + }; + 8C8E7744173064A9F6D438E3 /* Pods */ = { + isa = PBXGroup; + children = ( + 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */, + 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */, + 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */, + F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 50ADFFA52020D75100D50D53 /* Capacitor.framework */, + 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */, + F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 50ADFF85201F53D600D50D53 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF99201F53D600D50D53 /* Plugin.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 50ADFF87201F53D600D50D53 /* Plugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */; + buildPhases = ( + AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */, + 50ADFF83201F53D600D50D53 /* Sources */, + 50ADFF84201F53D600D50D53 /* Frameworks */, + 50ADFF85201F53D600D50D53 /* Headers */, + 50ADFF86201F53D600D50D53 /* Resources */, + AE646EB3107D841B880D174A /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Plugin; + productName = Plugin; + productReference = 50ADFF88201F53D600D50D53 /* Plugin.framework */; + productType = "com.apple.product-type.framework"; + }; + 50ADFF90201F53D600D50D53 /* PluginTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */; + buildPhases = ( + 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */, + 50ADFF8D201F53D600D50D53 /* Sources */, + 50ADFF8E201F53D600D50D53 /* Frameworks */, + 50ADFF8F201F53D600D50D53 /* Resources */, + CCA81D3B7E26D0D727D24C84 /* [CP] Embed Pods Frameworks */, + 32BFB60F8ADE8D433EDE204C /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 50ADFF94201F53D600D50D53 /* PBXTargetDependency */, + ); + name = PluginTests; + productName = PluginTests; + productReference = 50ADFF91201F53D600D50D53 /* PluginTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 50ADFF7F201F53D600D50D53 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0920; + ORGANIZATIONNAME = "Max Lynch"; + TargetAttributes = { + 50ADFF87201F53D600D50D53 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 0920; + ProvisioningStyle = Automatic; + }; + 50ADFF90201F53D600D50D53 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 50ADFF7E201F53D600D50D53; + productRefGroup = 50ADFF89201F53D600D50D53 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 50ADFF87201F53D600D50D53 /* Plugin */, + 50ADFF90201F53D600D50D53 /* PluginTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 50ADFF86201F53D600D50D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8F201F53D600D50D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-PluginTests-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; + }; + 32BFB60F8ADE8D433EDE204C /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Plugin-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; + }; + AE646EB3107D841B880D174A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + CCA81D3B7E26D0D727D24C84 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Capacitor/Capacitor.framework", + "${BUILT_PRODUCTS_DIR}/CapacitorCordova/Cordova.framework", + "${BUILT_PRODUCTS_DIR}/GCDWebServer/GCDWebServer.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Capacitor.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Cordova.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GCDWebServer.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 50ADFF83201F53D600D50D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50E1A94820377CB70090CE1A /* Plugin.swift in Sources */, + 50ADFFA82020EE4F00D50D53 /* Plugin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8D201F53D600D50D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF97201F53D600D50D53 /* PluginTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 50ADFF94201F53D600D50D53 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 50ADFF87201F53D600D50D53 /* Plugin */; + targetProxy = 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 50ADFF9A201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_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_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 50ADFF9B201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_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_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 50ADFF9D201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Plugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)"; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 50ADFF9E201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Plugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 50ADFFA0201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 50ADFFA1201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFF9A201F53D600D50D53 /* Debug */, + 50ADFF9B201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFF9D201F53D600D50D53 /* Debug */, + 50ADFF9E201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFFA0201F53D600D50D53 /* Debug */, + 50ADFFA1201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 50ADFF7F201F53D600D50D53 /* Project object */; +} diff --git a/ios/Plugin/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Plugin/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..23dacd5 --- /dev/null +++ b/ios/Plugin/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Plugin/Plugin.xcodeproj/project.xcworkspace/xcuserdata/max.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Plugin/Plugin.xcodeproj/project.xcworkspace/xcuserdata/max.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..7be5023 Binary files /dev/null and b/ios/Plugin/Plugin.xcodeproj/project.xcworkspace/xcuserdata/max.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/Plugin/Plugin.xcodeproj/xcuserdata/max.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/Plugin/Plugin.xcodeproj/xcuserdata/max.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..28229a6 --- /dev/null +++ b/ios/Plugin/Plugin.xcodeproj/xcuserdata/max.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Plugin.xcscheme + + orderHint + 5 + + + + diff --git a/ios/Plugin/Plugin.xcworkspace/contents.xcworkspacedata b/ios/Plugin/Plugin.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..afad624 --- /dev/null +++ b/ios/Plugin/Plugin.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Plugin/Plugin.xcworkspace/xcuserdata/max.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Plugin/Plugin.xcworkspace/xcuserdata/max.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..2df9265 Binary files /dev/null and b/ios/Plugin/Plugin.xcworkspace/xcuserdata/max.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/Plugin/Plugin.xcworkspace/xcuserdata/max.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/ios/Plugin/Plugin.xcworkspace/xcuserdata/max.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..08b6dd6 --- /dev/null +++ b/ios/Plugin/Plugin.xcworkspace/xcuserdata/max.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/ios/Plugin/Plugin/Info.plist b/ios/Plugin/Plugin/Info.plist new file mode 100644 index 0000000..1007fd9 --- /dev/null +++ b/ios/Plugin/Plugin/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/ios/Plugin/Plugin/Plugin.h b/ios/Plugin/Plugin/Plugin.h new file mode 100644 index 0000000..f2bd9e0 --- /dev/null +++ b/ios/Plugin/Plugin/Plugin.h @@ -0,0 +1,10 @@ +#import + +//! Project version number for Plugin. +FOUNDATION_EXPORT double PluginVersionNumber; + +//! Project version string for Plugin. +FOUNDATION_EXPORT const unsigned char PluginVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + diff --git a/ios/Plugin/Plugin/Plugin.m b/ios/Plugin/Plugin/Plugin.m new file mode 100644 index 0000000..a505a55 --- /dev/null +++ b/ios/Plugin/Plugin/Plugin.m @@ -0,0 +1,14 @@ +#import +#import + +// Define the plugin using the CAP_PLUGIN Macro, and +// each method the plugin supports using the CAP_PLUGIN_METHOD macro. + +CAP_PLUGIN(MediaPlugin, "MediaPlugin", + CAP_PLUGIN_METHOD(getMedias, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(getAlbums, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(createAlbum, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(savePhoto, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(saveVideo, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(saveGif, CAPPluginReturnPromise); +) diff --git a/ios/Plugin/Plugin/Plugin.swift b/ios/Plugin/Plugin/Plugin.swift new file mode 100644 index 0000000..6146b18 --- /dev/null +++ b/ios/Plugin/Plugin/Plugin.swift @@ -0,0 +1,361 @@ +import Foundation +import Photos +import Capacitor + +public class JSDate { + static func toString(_ date: Date) -> String { + let formatter = ISO8601DateFormatter() + return formatter.string(from: date) + } +} + +/** + * Please read the Capacitor iOS Plugin Development Guide + * here: https://capacitor.ionicframework.com/docs/plugins/ios + */ +@objc(MediaPlugin) +public class MediaPlugin: CAPPlugin { + typealias JSObject = [String:Any] + static let DEFAULT_QUANTITY = 25 + static let DEFAULT_TYPES = "photos" + static let DEFAULT_THUMBNAIL_WIDTH = 256 + static let DEFAULT_THUMBNAIL_HEIGHT = 256 + + // Must be lazy here because it will prompt for permissions on instantiation without it + lazy var imageManager = PHCachingImageManager() + + @objc func getAlbums(_ call: CAPPluginCall) { + checkAuthorization(allowed: { + self.fetchAlbumsToJs(call) + }, notAllowed: { + call.error("Access to photos not allowed by user") + }) + } + + @objc func getMedias(_ call: CAPPluginCall) { + checkAuthorization(allowed: { + self.fetchResultAssetsToJs(call) + }, notAllowed: { + call.error("Access to photos not allowed by user") + }) + } + + @objc func createAlbum(_ call: CAPPluginCall) { + guard let name = call.getString("name") else { + call.error("Must provide a name") + return + } + + checkAuthorization(allowed: { + PHPhotoLibrary.shared().performChanges({ + PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name) + }, completionHandler: { success, error in + if !success { + call.error("Unable to create album", error) + return + } + call.success() + }) + }, notAllowed: { + call.error("Access to photos not allowed by user") + }) + } + + @objc func savePhoto(_ call: CAPPluginCall) { + guard let data = call.getString("path") else { + call.error("Must provide the data path") + return + } + + let albumId = call.getString("album") + var targetCollection: PHAssetCollection? + + if albumId != nil { + let albumFetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId!], options: nil) + albumFetchResult.enumerateObjects({ (collection, count, _) in + targetCollection = collection + }) + if targetCollection == nil { + call.error("Unable to find that album") + return + } + if !targetCollection!.canPerform(.addContent) { + call.error("Album doesn't support adding content (is this a smart album?)") + return + } + } + + checkAuthorization(allowed: { + // Add it to the photo library. + PHPhotoLibrary.shared().performChanges({ + + let creationRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: URL(string: data)!) + + if let collection = targetCollection { + let addAssetRequest = PHAssetCollectionChangeRequest(for: collection) + addAssetRequest?.addAssets([creationRequest?.placeholderForCreatedAsset! as Any] as NSArray) + } + + }, completionHandler: {success, error in + if !success { + call.error("Unable to save image to album", error) + } else { + call.success() + } + }) + }, notAllowed: { + call.error("Access to photos not allowed by user") + }) + + } + + @objc func saveVideo(_ call: CAPPluginCall) { + guard let data = call.getString("path") else { + call.error("Must provide the data path") + return + } + + let albumId = call.getString("album") + var targetCollection: PHAssetCollection? + + if albumId != nil { + let albumFetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId!], options: nil) + albumFetchResult.enumerateObjects({ (collection, count, _) in + targetCollection = collection + }) + if targetCollection == nil { + call.error("Unable to find that album") + return + } + if !targetCollection!.canPerform(.addContent) { + call.error("Album doesn't support adding content (is this a smart album?)") + return + } + } + + checkAuthorization(allowed: { + // Add it to the photo library. + PHPhotoLibrary.shared().performChanges({ + let creationRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(string: data)!) + + if let collection = targetCollection { + let addAssetRequest = PHAssetCollectionChangeRequest(for: collection) + addAssetRequest?.addAssets([creationRequest?.placeholderForCreatedAsset! as Any] as NSArray) + } + }, completionHandler: {success, error in + if !success { + call.error("Unable to save video to album", error) + } else { + call.success() + } + }) + }, notAllowed: { + call.error("Access to photos not allowed by user") + }) + + } + + @objc func saveGif(_ call: CAPPluginCall) { + guard let data = call.getString("path") else { + call.error("Must provide the data path") + return + } + + let albumId = call.getString("album") + var targetCollection: PHAssetCollection? + + if albumId != nil { + let albumFetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId!], options: nil) + albumFetchResult.enumerateObjects({ (collection, count, _) in + targetCollection = collection + }) + if targetCollection == nil { + call.error("Unable to find that album") + return + } + if !targetCollection!.canPerform(.addContent) { + call.error("Album doesn't support adding content (is this a smart album?)") + return + } + } + + checkAuthorization(allowed: { + // Add it to the photo library. + PHPhotoLibrary.shared().performChanges({ + + let creationRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: URL(string: data)!) + + if let collection = targetCollection { + let addAssetRequest = PHAssetCollectionChangeRequest(for: collection) + addAssetRequest?.addAssets([creationRequest?.placeholderForCreatedAsset! as Any] as NSArray) + } + + }, completionHandler: {success, error in + if !success { + call.error("Unable to save gif to album", error) + } else { + call.success() + } + }) + }, notAllowed: { + call.error("Access to photos not allowed by user") + }) + } + + func checkAuthorization(allowed: @escaping () -> Void, notAllowed: @escaping () -> Void) { + let status = PHPhotoLibrary.authorizationStatus() + if status == PHAuthorizationStatus.authorized { + allowed() + } else { + PHPhotoLibrary.requestAuthorization({ (newStatus) in + if newStatus == PHAuthorizationStatus.authorized { + allowed() + } else { + notAllowed() + } + }) + } + } + + func fetchAlbumsToJs(_ call: CAPPluginCall) { + var albums = [JSObject]() + + let loadSharedAlbums = call.getBool("loadShared", false)! + + // Load our smart albums + var fetchResult = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil) + fetchResult.enumerateObjects({ (collection, count, stop: UnsafeMutablePointer) in + var o = JSObject() + o["name"] = collection.localizedTitle + o["identifier"] = collection.localIdentifier + o["type"] = "smart" + albums.append(o) + }) + + if loadSharedAlbums { + fetchResult = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumCloudShared, options: nil) + fetchResult.enumerateObjects({ (collection, count, stop: UnsafeMutablePointer) in + var o = JSObject() + o["name"] = collection.localizedTitle + o["identifier"] = collection.localIdentifier + o["type"] = "shared" + albums.append(o) + }) + } + + // Load our user albums + PHCollectionList.fetchTopLevelUserCollections(with: nil).enumerateObjects({ (collection, count, stop: UnsafeMutablePointer) in + var o = JSObject() + o["name"] = collection.localizedTitle + o["identifier"] = collection.localIdentifier + o["type"] = "user" + albums.append(o) + }) + + call.success([ + "albums": albums + ]) + } + + func fetchResultAssetsToJs(_ call: CAPPluginCall) { + var assets: [JSObject] = [] + + let albumId = call.getString("albumIdentifier") + + let quantity = call.getInt("quantity", MediaPlugin.DEFAULT_QUANTITY)! + + var targetCollection: PHAssetCollection? + + let options = PHFetchOptions() + options.fetchLimit = quantity + options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] + + if albumId != nil { + let albumFetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId!], options: nil) + albumFetchResult.enumerateObjects({ (collection, count, _) in + targetCollection = collection + }) + } + + var fetchResult: PHFetchResult; + if targetCollection != nil { + fetchResult = PHAsset.fetchAssets(in: targetCollection!, options: options) + } else { + fetchResult = PHAsset.fetchAssets(with: options) + } + + //let after = call.getString("after") + + let types = call.getString("types") ?? MediaPlugin.DEFAULT_TYPES + let thumbnailWidth = call.getInt("thumbnailWidth", MediaPlugin.DEFAULT_THUMBNAIL_WIDTH)! + let thumbnailHeight = call.getInt("thumbnailHeight", MediaPlugin.DEFAULT_THUMBNAIL_HEIGHT)! + let thumbnailSize = CGSize(width: thumbnailWidth, height: thumbnailHeight) + let thumbnailQuality = call.getInt("thumbnailQuality", 95)! + + let requestOptions = PHImageRequestOptions() + requestOptions.isNetworkAccessAllowed = true + requestOptions.version = .current + requestOptions.deliveryMode = .opportunistic + requestOptions.isSynchronous = true + + fetchResult.enumerateObjects({ (asset, count: Int, stop: UnsafeMutablePointer) in + + if asset.mediaType == .image && types == "videos" { + return + } + if asset.mediaType == .video && types == "photos" { + return + } + + var a = JSObject() + + self.imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: requestOptions, resultHandler: { (fetchedImage, _) in + guard let image = fetchedImage else { + return + } + + a["identifier"] = asset.localIdentifier + + // TODO: We need to know original type + a["data"] = image.jpegData(compressionQuality: CGFloat(thumbnailQuality) / 100.0)?.base64EncodedString() + + if asset.creationDate != nil { + a["creationDate"] = JSDate.toString(asset.creationDate!) + } + a["fullWidth"] = asset.pixelWidth + a["fullHeight"] = asset.pixelHeight + a["thumbnailWidth"] = image.size.width + a["thumbnailHeight"] = image.size.height + a["location"] = self.makeLocation(asset) + + assets.append(a) + }) + }) + + call.success([ + "medias": assets + ]) + } + + + func makeLocation(_ asset: PHAsset) -> JSObject { + var loc = JSObject() + guard let location = asset.location else { + return loc + } + + loc["latitude"] = location.coordinate.latitude + loc["longitude"] = location.coordinate.longitude + loc["altitude"] = location.altitude + loc["heading"] = location.course + loc["speed"] = location.speed + return loc + } + + + /* + deinit { + PHPhotoLibrary.shared().unregisterChangeObserver(self) + } + */ +} diff --git a/ios/Plugin/PluginTests/Info.plist b/ios/Plugin/PluginTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/ios/Plugin/PluginTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/ios/Plugin/PluginTests/PluginTests.swift b/ios/Plugin/PluginTests/PluginTests.swift new file mode 100644 index 0000000..00f036e --- /dev/null +++ b/ios/Plugin/PluginTests/PluginTests.swift @@ -0,0 +1,35 @@ +import XCTest +import Capacitor +@testable import Plugin + +class PluginTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testEcho() { + // This is an example of a functional test case for a plugin. + // Use XCTAssert and related functions to verify your tests produce the correct results. + + let value = "Hello, World!" + let plugin = MyPlugin() + + let call = CAPPluginCall(callbackId: "test", options: [ + "value": value + ], success: { (result, call) in + let resultValue = result!.data["value"] as? String + XCTAssertEqual(value, resultValue) + }, error: { (err) in + XCTFail("Error shouldn't have been called") + }) + + plugin.echo(call!) + } +} diff --git a/ios/Plugin/Podfile b/ios/Plugin/Podfile new file mode 100644 index 0000000..2c5b62c --- /dev/null +++ b/ios/Plugin/Podfile @@ -0,0 +1,16 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '11.0' + +target 'Plugin' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + + # Pods for IonicRunner + pod 'Capacitor' +end + +target 'PluginTests' do + use_frameworks! + + pod 'Capacitor' +end \ No newline at end of file diff --git a/ios/Plugin/Podfile.lock b/ios/Plugin/Podfile.lock new file mode 100644 index 0000000..487d066 --- /dev/null +++ b/ios/Plugin/Podfile.lock @@ -0,0 +1,20 @@ +PODS: + - Capacitor (0.0.8): + - CapacitorCordova (= 0.0.8) + - GCDWebServer (~> 3.0) + - CapacitorCordova (0.0.8) + - GCDWebServer (3.4.2): + - GCDWebServer/Core (= 3.4.2) + - GCDWebServer/Core (3.4.2) + +DEPENDENCIES: + - Capacitor + +SPEC CHECKSUMS: + Capacitor: 36a275e2fd85b69a2cb9633e8920ecf5c5aab88d + CapacitorCordova: cf26a9b075eb1f339118c920a975e4ff9e403193 + GCDWebServer: 8d67ee9f634b4bb91eb4b8aee440318a5fc6debd + +PODFILE CHECKSUM: 57e6d463d83fe9e2081ca17be52456b12c5e148c + +COCOAPODS: 1.4.0 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..25c1f5a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "capacitor-media", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@capacitor/core": { + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-1.0.0-beta.19.tgz", + "integrity": "sha512-ANpQx+pq0g/fYCTzYWEVZxwkzXqOWviVXl7s/UPAZUdRhAA4S7Iaori+fatvVQnHPhmXy00ezzQidXqiuedpGg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e360a73 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "capacitor-media", + "version": "0.0.1", + "description": "Enable some media features for Capacitor such as create albums, save videos and gifs.", + "main": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "scripts": { + "build": "npm run clean && tsc", + "clean": "rm -rf ./dist", + "watch": "tsc --watch", + "prepublishOnly": "npm run build", + "release:custom": "standard-version release --release-as x.x.x", + "release:patch": "standard-version release --release-as patch", + "release:minor": "standard-version release --release-as minor", + "release:major": "standard-version release --release-as major" + }, + "author": "Stewan Silva", + "license": "MIT", + "dependencies": { + "@capacitor/core": "latest" + }, + "devDependencies": { + "typescript": "^2.6.2" + }, + "files": [ + "dist/", + "ios/", + "android/", + "CapacitorMedia.podspec" + ], + "keywords": [ + "capacitor", + "plugin", + "native" + ], + "capacitor": { + "ios": { + "src": "ios" + }, + "android": { + "src": "android" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/stewwan/capacitor-media" + }, + "bugs": { + "url": "https://github.com/stewwan/capacitor-media/issues" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..906e8a1 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,14 @@ +import nodeResolve from 'rollup-plugin-node-resolve'; + +export default { + input: 'dist/esm/index.js', + output: { + file: 'dist/plugin.js', + format: 'iife', + name: 'capacitorPlugin', + sourcemap: true + }, + plugins: [ + nodeResolve() + ] +}; \ No newline at end of file diff --git a/src/definitions.ts b/src/definitions.ts new file mode 100644 index 0000000..c0bde50 --- /dev/null +++ b/src/definitions.ts @@ -0,0 +1,138 @@ +declare global { + interface PluginRegistry { + MediaPlugin?: IMediaPlugin; + } +} + +export interface MediaSaveOptions { + path: string; + album?: string; +} + +export interface MediaAlbumCreate { + name: string; +} + +export declare enum MediaAlbumType { + /** + * Album is a "smart" album (such as Favorites or Recently Added) + */ + Smart = "smart", + /** + * Album is a cloud-shared album + */ + Shared = "shared", + /** + * Album is a user-created album + */ + User = "user" +} + +export interface MediaLocation { + /** + * GPS latitude image was taken at + */ + latitude: number; + /** + * GPS longitude image was taken at + */ + longitude: number; + /** + * Heading of user at time image was taken + */ + heading: number; + /** + * Altitude of user at time image was taken + */ + altitude: number; + /** + * Speed of user at time image was taken + */ + speed: number; +} + +export interface MediaAlbum { + identifier: string; + name: string; + count: number; + type: MediaAlbumType; +} + +export interface MediaAlbumResponse { + albums: MediaAlbum[]; +} + +export interface MediaResponse { + medias: MediaAsset[]; +} + +export interface MediaAsset { + /** + * Platform-specific identifier + */ + identifier: string; + /** + * Data for a photo asset as a base64 encoded string (JPEG only supported) + */ + data: string; + /** + * ISO date string for creation date of asset + */ + creationDate: string; + /** + * Full width of original asset + */ + fullWidth: number; + /** + * Full height of original asset + */ + fullHeight: number; + /** + * Width of thumbnail preview + */ + thumbnailWidth: number; + /** + * Height of thumbnail preview + */ + thumbnailHeight: number; + /** + * Location metadata for the asset + */ + location: MediaLocation; +} + +export interface MediaFetchOptions { + /** + * The number of photos to fetch, sorted by last created date descending + */ + quantity?: number; + /** + * The width of thumbnail to return + */ + thumbnailWidth?: number; + /** + * The height of thumbnail to return + */ + thumbnailHeight?: number; + /** + * The quality of thumbnail to return as JPEG (0-100) + */ + thumbnailQuality?: number; + /** + * Which types of assets to return (currently only supports "photos") + */ + types?: string; + /** + * Which album identifier to query in (get identifier with getAlbums()) + */ + albumIdentifier?: string; +} + +export interface IMediaPlugin { + getMedias(options?: MediaFetchOptions): Promise; + getAlbums(): Promise; + savePhoto(options?: MediaSaveOptions): Promise; + saveVideo(options?: MediaSaveOptions): Promise; + saveGif(options?: MediaSaveOptions): Promise; + createAlbum(options: MediaAlbumCreate): Promise; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..378647f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from "./definitions"; +export * from "./plugin"; +// export * from './web'; //@todo diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..1f61854 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,151 @@ +export interface PhotosFetchOptions { + /** + * The number of photos to fetch, sorted by last created date descending + */ + quantity?: number; + /** + * The width of thumbnail to return + */ + thumbnailWidth?: number; + /** + * The height of thumbnail to return + */ + thumbnailHeight?: number; + /** + * The quality of thumbnail to return as JPEG (0-100) + */ + thumbnailQuality?: number; + /** + * Which types of assets to return (currently only supports "photos") + */ + types?: string; + /** + * Which album identifier to query in (get identifier with getAlbums()) + */ + albumIdentifier?: string; +} +export interface PhotoAsset { + /** + * Platform-specific identifier + */ + identifier: string; + /** + * Data for a photo asset as a base64 encoded string (JPEG only supported) + */ + data: string; + /** + * ISO date string for creation date of asset + */ + creationDate: string; + /** + * Full width of original asset + */ + fullWidth: number; + /** + * Full height of original asset + */ + fullHeight: number; + /** + * Width of thumbnail preview + */ + thumbnailWidth: number; + /** + * Height of thumbnail preview + */ + thumbnailHeight: number; + /** + * Location metadata for the asset + */ + location: PhotoLocation; +} +export interface PhotoLocation { + /** + * GPS latitude image was taken at + */ + latitude: number; + /** + * GPS longitude image was taken at + */ + longitude: number; + /** + * Heading of user at time image was taken + */ + heading: number; + /** + * Altitude of user at time image was taken + */ + altitude: number; + /** + * Speed of user at time image was taken + */ + speed: number; +} +export interface PhotosResult { + /** + * The list of photos returned from the library + */ + photos: PhotoAsset[]; +} +export interface PhotosSaveOptions { + /** + * The base64-encoded JPEG data for a photo (note: do not add HTML data-uri type prefix) + */ + data: string; + /** + * The optional album identifier to save this photo in + */ + albumIdentifier?: string; +} +export interface PhotosSaveResult { + /** + * Whether the photo was created + */ + success: boolean; +} +export interface PhotosAlbumsFetchOptions { + /** + * Whether to load cloud shared albums + */ + loadShared: boolean; +} +export interface PhotosAlbumsResult { + /** + * The list of albums returned from the query + */ + albums: PhotosAlbum[]; +} +export interface PhotosAlbum { + /** + * Local identifier for the album + */ + identifier: string; + /** + * Name of the album + */ + name: string; + /** + * Number of items in the album + */ + count: number; + /** + * The type of album + */ + type: PhotosAlbumType; +} +export interface PhotosCreateAlbumOptions { + name: string; +} +export declare enum PhotosAlbumType { + /** + * Album is a "smart" album (such as Favorites or Recently Added) + */ + Smart = "smart", + /** + * Album is a cloud-shared album + */ + Shared = "shared", + /** + * Album is a user-created album + */ + User = "user" +} diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 0000000..e041448 --- /dev/null +++ b/src/plugin.ts @@ -0,0 +1,37 @@ +import { Plugins } from "@capacitor/core"; +import { + IMediaPlugin, + MediaFetchOptions, + MediaResponse, + MediaAlbumResponse, + MediaSaveOptions, + MediaAlbumCreate +} from "./definitions"; + +const { MediaPlugin } = Plugins; + +export class Media implements IMediaPlugin { + getMedias(options?: MediaFetchOptions): Promise { + return MediaPlugin.getMedias(options); + } + + getAlbums(): Promise { + return MediaPlugin.getAlbums(); + } + + savePhoto(options?: MediaSaveOptions): Promise { + return MediaPlugin.savePhoto(options); + } + + saveVideo(options?: MediaSaveOptions): Promise { + return MediaPlugin.saveVideo(options); + } + + saveGif(options?: MediaSaveOptions): Promise { + return MediaPlugin.saveGif(options); + } + + createAlbum(options: MediaAlbumCreate): Promise { + return MediaPlugin.createAlbum(options); + } +} diff --git a/src/web.ts b/src/web.ts new file mode 100644 index 0000000..4080d81 --- /dev/null +++ b/src/web.ts @@ -0,0 +1,20 @@ +// import { WebPlugin } from '@capacitor/core'; +// import { MediaPlugin } from './definitions'; + +// export class MediaPluginWeb extends WebPlugin implements MediaPlugin { +// constructor() { +// super({ +// name: 'MediaPlugin', +// platforms: ['web'] +// }); +// } + +// async echo(options: { value: string }): Promise<{value: string}> { +// console.log('ECHO', options); +// return options; +// } +// } + +// const MediaPlugin = new MediaPluginWeb(); + +// export { MediaPlugin }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0a9d4e8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "experimentalDecorators": true, + "noEmitHelpers": true, + "importHelpers": true, + "moduleResolution": "node", + "lib": ["dom", "es2015"], + "module": "es2015", + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist/esm", + "sourceMap": true, + "target": "es2015" + }, + "files": ["src/index.ts"] +}