From ea077852a132d5eb62332a19e891afc21c9ae2cb Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sun, 19 Nov 2017 15:18:07 +0000 Subject: [PATCH] Mobile: allow attaching image or any other file --- ReactNativeClient/android/app/build.gradle | 1 + .../android/app/src/main/AndroidManifest.xml | 63 +++++----- .../net/cozic/joplin/MainApplication.java | 2 + ReactNativeClient/android/build.gradle | 2 +- ReactNativeClient/android/settings.gradle | 2 + .../ios/Joplin.xcodeproj/project.pbxproj | 18 ++- ReactNativeClient/ios/Joplin/Info.plist | 4 + .../lib/components/screens/note.js | 116 ++++++++++++------ ReactNativeClient/package-lock.json | 5 + ReactNativeClient/package.json | 1 + 10 files changed, 140 insertions(+), 74 deletions(-) diff --git a/ReactNativeClient/android/app/build.gradle b/ReactNativeClient/android/app/build.gradle index 5ca5f5a48..204354982 100644 --- a/ReactNativeClient/android/app/build.gradle +++ b/ReactNativeClient/android/app/build.gradle @@ -137,6 +137,7 @@ android { } dependencies { + compile project(':react-native-image-picker') compile project(':react-native-vector-icons') compile project(':react-native-fs') compile fileTree(dir: "libs", include: ["*.jar"]) diff --git a/ReactNativeClient/android/app/src/main/AndroidManifest.xml b/ReactNativeClient/android/app/src/main/AndroidManifest.xml index aec1174cf..1f233c49f 100644 --- a/ReactNativeClient/android/app/src/main/AndroidManifest.xml +++ b/ReactNativeClient/android/app/src/main/AndroidManifest.xml @@ -1,38 +1,37 @@ + xmlns:tools="http://schemas.android.com/tools" + package="net.cozic.joplin" + android:versionCode="2" + android:versionName="0.8.0"> - - - - - + + + + + - + - + - - - - - - - - - - + android:configChanges="keyboard|keyboardHidden|orientation|screenSize" + android:windowSoftInputMode="adjustResize" + android:launchMode="singleInstance"> + + + + + + + + diff --git a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/MainApplication.java b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/MainApplication.java index 9de2b477e..f0fed230f 100644 --- a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/MainApplication.java +++ b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/MainApplication.java @@ -3,6 +3,7 @@ package net.cozic.joplin; import android.app.Application; import com.facebook.react.ReactApplication; +import com.imagepicker.ImagePickerPackage; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainReactPackage; @@ -30,6 +31,7 @@ public class MainApplication extends Application implements ReactApplication { return Arrays.asList( new ImageResizerPackage(), new MainReactPackage(), + new ImagePickerPackage(), new ReactNativeDocumentPicker(), new RNFetchBlobPackage(), new RNFSPackage(), diff --git a/ReactNativeClient/android/build.gradle b/ReactNativeClient/android/build.gradle index eed9972b5..7b46d23f4 100644 --- a/ReactNativeClient/android/build.gradle +++ b/ReactNativeClient/android/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:2.2.+' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/ReactNativeClient/android/settings.gradle b/ReactNativeClient/android/settings.gradle index 4f60d1d27..7bf4ddbd7 100644 --- a/ReactNativeClient/android/settings.gradle +++ b/ReactNativeClient/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'Joplin' +include ':react-native-image-picker' +project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android') include ':react-native-vector-icons' project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') include ':react-native-image-resizer' diff --git a/ReactNativeClient/ios/Joplin.xcodeproj/project.pbxproj b/ReactNativeClient/ios/Joplin.xcodeproj/project.pbxproj index d87363aa0..c3f4ee937 100644 --- a/ReactNativeClient/ios/Joplin.xcodeproj/project.pbxproj +++ b/ReactNativeClient/ios/Joplin.xcodeproj/project.pbxproj @@ -5,7 +5,6 @@ }; objectVersion = 46; objects = { - /* Begin PBXBuildFile section */ 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; @@ -55,6 +54,7 @@ EA501DCDCF4745E9B63ECE98 /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7D46CBDF8846409890AD7A84 /* Octicons.ttf */; }; EC11356C90E9419799A2626F /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 51BCEC3BC28046C8BB19531F /* EvilIcons.ttf */; }; FBF57CE2F0F448FA9A8985E2 /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB8BCAEA9AA41CAAE460443 /* libsqlite3.0.tbd */; }; + AF99EEC6C55042F7BFC87583 /* libRNImagePicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 44A39642217548C8ADA91CBA /* libRNImagePicker.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -404,6 +404,8 @@ F5E37D05726A4A08B2EE323A /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = ""; }; FD370E24D76E461D960DD85D /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = ""; }; FF411B45E68B4A8CBCC35777 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = ""; }; + A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */ = {isa = PBXFileReference; name = "RNImagePicker.xcodeproj"; path = "../node_modules/react-native-image-picker/ios/RNImagePicker.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + 44A39642217548C8ADA91CBA /* libRNImagePicker.a */ = {isa = PBXFileReference; name = "libRNImagePicker.a"; path = "libRNImagePicker.a"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -439,6 +441,7 @@ 929CA5ABC80E4D398AFC4E44 /* libSQLite.a in Frameworks */, FBF57CE2F0F448FA9A8985E2 /* libsqlite3.0.tbd in Frameworks */, AE6BB3A2FDA34D01864A721A /* libRNVectorIcons.a in Frameworks */, + AF99EEC6C55042F7BFC87583 /* libRNImagePicker.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -706,6 +709,7 @@ DF1C50EBC11E46A3AF87F80A /* RCTImageResizer.xcodeproj */, CCDE9E9AF09B45F391B1C2AF /* SQLite.xcodeproj */, 711CBD21F0894B83A2D8E234 /* RNVectorIcons.xcodeproj */, + A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -1392,6 +1396,7 @@ "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", "$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios", "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", + "$(SRCROOT)\..\node_modules\react-native-image-picker\ios", ); INFOPLIST_FILE = JoplinTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1404,6 +1409,7 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -1428,6 +1434,7 @@ "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", "$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios", "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", + "$(SRCROOT)\..\node_modules\react-native-image-picker\ios", ); INFOPLIST_FILE = JoplinTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1440,6 +1447,7 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -1465,6 +1473,7 @@ "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", "$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios", "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", + "$(SRCROOT)\..\node_modules\react-native-image-picker\ios", ); INFOPLIST_FILE = Joplin/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1492,6 +1501,7 @@ "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", "$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios", "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", + "$(SRCROOT)\..\node_modules\react-native-image-picker\ios", ); INFOPLIST_FILE = Joplin/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1526,6 +1536,7 @@ "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", "$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios", "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", + "$(SRCROOT)\..\node_modules\react-native-image-picker\ios", ); INFOPLIST_FILE = "Joplin-tvOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1537,6 +1548,7 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -1571,6 +1583,7 @@ "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", "$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios", "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", + "$(SRCROOT)\..\node_modules\react-native-image-picker\ios", ); INFOPLIST_FILE = "Joplin-tvOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1582,6 +1595,7 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -1616,6 +1630,7 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.Joplin-tvOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1646,6 +1661,7 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.Joplin-tvOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ReactNativeClient/ios/Joplin/Info.plist b/ReactNativeClient/ios/Joplin/Info.plist index a973da938..3062cfbca 100644 --- a/ReactNativeClient/ios/Joplin/Info.plist +++ b/ReactNativeClient/ios/Joplin/Info.plist @@ -51,6 +51,10 @@ + NSPhotoLibraryUsageDescription + To allow attaching images to a note + NSCameraUsageDescription + To allow attaching a photo to a note UIAppFonts Entypo.ttf diff --git a/ReactNativeClient/lib/components/screens/note.js b/ReactNativeClient/lib/components/screens/note.js index 359323861..03c82041a 100644 --- a/ReactNativeClient/lib/components/screens/note.js +++ b/ReactNativeClient/lib/components/screens/note.js @@ -25,6 +25,7 @@ const RNFetchBlob = require('react-native-fetch-blob').default; const { DocumentPicker, DocumentPickerUtil } = require('react-native-document-picker'); const ImageResizer = require('react-native-image-resizer').default; const shared = require('lib/components/shared/note-screen-shared.js'); +const ImagePicker = require('react-native-image-picker'); class NoteScreenComponent extends BaseScreenComponent { @@ -185,7 +186,7 @@ class NoteScreenComponent extends BaseScreenComponent { async pickDocument() { return new Promise((resolve, reject) => { - DocumentPicker.show({ filetype: [DocumentPickerUtil.images()] }, (error,res) => { + DocumentPicker.show({ filetype: [DocumentPickerUtil.allFiles()] }, (error,res) => { if (error) { // Also returns an error if the user doesn't pick a file // so just resolve with null. @@ -207,56 +208,77 @@ class NoteScreenComponent extends BaseScreenComponent { }); } - async attachFile_onPress() { - const res = await this.pickDocument(); - if (!res) { - reg.logger().info('Did not get any file (user cancel?)'); + showImagePicker(options) { + return new Promise((resolve, reject) => { + ImagePicker.showImagePicker(options, (response) => { + resolve(response); + }); + }); + } + + async resizeImage(localFilePath, targetPath, mimeType) { + const maxSize = Resource.IMAGE_MAX_DIMENSION; + + let dimensions = await this.imageDimensions(localFilePath); + + reg.logger().info('Original dimensions ', dimensions); + if (dimensions.width > maxSize || dimensions.height > maxSize) { + dimensions.width = maxSize; + dimensions.height = maxSize; + } + reg.logger().info('New dimensions ', dimensions); + + const format = mimeType == 'image/png' ? 'PNG' : 'JPEG'; + reg.logger().info('Resizing image ' + localFilePath); + const resizedImage = await ImageResizer.createResizedImage(localFilePath, dimensions.width, dimensions.height, format, 85); + const resizedImagePath = resizedImage.uri; + reg.logger().info('Resized image ', resizedImagePath); + RNFetchBlob.fs.cp(resizedImagePath, targetPath); // mv doesn't work ("source path does not exist") so need to do cp and unlink + + try { + RNFetchBlob.fs.unlink(resizedImagePath); + } catch (error) { + reg.logger().info('Error when unlinking cached file: ', error); + } + } + + async attachFile(pickerResponse, fileType) { + if (!pickerResponse) { + reg.logger().warn('Got no response from picker'); return; } - const localFilePath = res.uri; + if (pickerResponse.error) { + reg.logger().warn('Got error from picker', pickerResponse.error); + return; + } + + if (pickerResponse.didCancel) { + reg.logger().info('User cancelled picker'); + return; + } + + const localFilePath = pickerResponse.uri; reg.logger().info('Got file: ' + localFilePath); - reg.logger().info('Got type: ' + res.type); - - // res.uri, - // res.type, // mime type - // res.fileName, - // res.fileSize + reg.logger().info('Got type: ' + pickerResponse.type); let resource = Resource.new(); resource.id = uuid.create(); - resource.mime = res.type; - resource.title = res.fileName ? res.fileName : _('Untitled'); + resource.mime = pickerResponse.type; + resource.title = pickerResponse.fileName ? pickerResponse.fileName : _('Untitled'); let targetPath = Resource.fullPath(resource); - if (res.type == 'image/jpeg' || res.type == 'image/jpg' || res.type == 'image/png') { - const maxSize = Resource.IMAGE_MAX_DIMENSION; - - let dimensions = await this.imageDimensions(localFilePath); - - reg.logger().info('Original dimensions ', dimensions); - if (dimensions.width > maxSize || dimensions.height > maxSize) { - dimensions.width = maxSize; - dimensions.height = maxSize; - } - reg.logger().info('New dimensions ', dimensions); - - const format = res.type == 'image/png' ? 'PNG' : 'JPEG'; - reg.logger().info('Resizing image ' + localFilePath); - const resizedImage = await ImageResizer.createResizedImage(localFilePath, dimensions.width, dimensions.height, format, 85); - const resizedImagePath = resizedImage.uri; - reg.logger().info('Resized image ', resizedImagePath); - RNFetchBlob.fs.cp(resizedImagePath, targetPath); // mv doesn't work ("source path does not exist") so need to do cp and unlink - - try { - RNFetchBlob.fs.unlink(resizedImagePath); - } catch (error) { - reg.logger().info('Error when unlinking cached file: ', error); - } + if (pickerResponse.type == 'image/jpeg' || pickerResponse.type == 'image/jpg' || pickerResponse.type == 'image/png') { + await this.resizeImage(localFilePath, targetPath, pickerResponse.mime); } else { - RNFetchBlob.fs.cp(localFilePath, targetPath); + if (fileType === 'image') { + dialogs.error(this, _('Unsupported image type: %s', pickerResponse.type)); + return; + } else { + RNFetchBlob.fs.cp(localFilePath, targetPath); + } } await Resource.save(resource, { isNew: true }); @@ -268,6 +290,19 @@ class NoteScreenComponent extends BaseScreenComponent { this.setState({ note: newNote }); } + async attachImage_onPress() { + const options = { + mediaType: 'photo', + }; + const response = await this.showImagePicker(options); + await this.attachFile(response, 'image'); + } + + async attachFile_onPress() { + const response = await this.pickDocument(); + await this.attachFile(response, 'all'); + } + toggleIsTodo_onPress() { shared.toggleIsTodo_onPress(this); } @@ -294,7 +329,8 @@ class NoteScreenComponent extends BaseScreenComponent { let output = []; - output.push({ title: _('Attach file'), onPress: () => { this.attachFile_onPress(); } }); + output.push({ title: _('Attach image'), onPress: () => { this.attachImage_onPress(); } }); + output.push({ title: _('Attach any other file'), onPress: () => { this.attachFile_onPress(); } }); output.push({ title: _('Delete note'), onPress: () => { this.deleteNote_onPress(); } }); // if (isTodo) { diff --git a/ReactNativeClient/package-lock.json b/ReactNativeClient/package-lock.json index c6dc8af0c..3e4a5f3a4 100644 --- a/ReactNativeClient/package-lock.json +++ b/ReactNativeClient/package-lock.json @@ -4645,6 +4645,11 @@ "utf8": "2.1.2" } }, + "react-native-image-picker": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-0.26.7.tgz", + "integrity": "sha1-rS7pV/f2zAE5aJPqA9hMsq2y43Y=" + }, "react-native-image-resizer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/react-native-image-resizer/-/react-native-image-resizer-1.0.0.tgz", diff --git a/ReactNativeClient/package.json b/ReactNativeClient/package.json index fcdce7afd..b2821cb87 100644 --- a/ReactNativeClient/package.json +++ b/ReactNativeClient/package.json @@ -24,6 +24,7 @@ "react-native-document-picker": "^2.1.0", "react-native-fetch-blob": "^0.10.6", "react-native-fs": "^2.8.1", + "react-native-image-picker": "^0.26.7", "react-native-image-resizer": "^1.0.0", "react-native-material-dropdown": "^0.5.2", "react-native-popup-dialog": "^0.9.35",