You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Attach resource from mobile
This commit is contained in:
		| @@ -164,6 +164,8 @@ function isImageMimeType(m) { | ||||
| } | ||||
|  | ||||
| function addResourceTag(lines, resource, alt = "") { | ||||
| 	// TODO: refactor to use Resource.markdownTag | ||||
|  | ||||
| 	let tagAlt = alt == "" ? resource.alt : alt; | ||||
| 	if (!tagAlt) tagAlt = ''; | ||||
| 	if (isImageMimeType(resource.mime)) { | ||||
|   | ||||
| @@ -143,6 +143,7 @@ dependencies { | ||||
| 	compile "com.facebook.react:react-native:+"  // From node_modules | ||||
| 	compile project(':react-native-sqlite-storage') | ||||
| 	compile project(':react-native-fetch-blob') | ||||
| 	compile project(':react-native-document-picker') | ||||
| } | ||||
|  | ||||
| // Run this once to be able to run the application with BUCK | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import com.facebook.react.shell.MainReactPackage; | ||||
| import com.facebook.soloader.SoLoader; | ||||
| import org.pgsqlite.SQLitePluginPackage; | ||||
| import com.RNFetchBlob.RNFetchBlobPackage; | ||||
| import com.reactnativedocumentpicker.ReactNativeDocumentPicker; | ||||
|  | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| @@ -28,7 +29,8 @@ public class MainApplication extends Application implements ReactApplication { | ||||
| 				new SQLitePluginPackage(), | ||||
| 				new MainReactPackage(), | ||||
| 				new RNFSPackage(), | ||||
| 				new RNFetchBlobPackage() | ||||
| 				new RNFetchBlobPackage(), | ||||
| 				new ReactNativeDocumentPicker() | ||||
| 			); | ||||
| 		} | ||||
| 	}; | ||||
|   | ||||
| @@ -8,4 +8,7 @@ include ':react-native-sqlite-storage' | ||||
| project(':react-native-sqlite-storage').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sqlite-storage/src/android') | ||||
|  | ||||
| include ':react-native-fetch-blob' | ||||
| project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android') | ||||
| project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android') | ||||
|  | ||||
| include ':react-native-document-picker' | ||||
| project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android') | ||||
| @@ -126,13 +126,14 @@ class NoteBodyViewer extends Component { | ||||
| 			} | ||||
|  | ||||
| 			const r = this.state.resources[resourceId]; | ||||
| 			if (r.mime == 'image/png' || r.mime == 'image/jpg' || r.mime == 'image/gif') { | ||||
| 			const mime = r.mime.toLowerCase(); | ||||
| 			if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') { | ||||
| 				const src = 'data:' + r.mime + ';base64,' + r.base64; | ||||
| 				let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>'; | ||||
| 				let output = '<img title="' + htmlentities(title) + '" src="' + htmlentities(src) + '"/>'; | ||||
| 				return output; | ||||
| 			} | ||||
| 			 | ||||
| 			return '[Image: ' + htmlentities(r.title) + '(' + htmlentities(r.mime) + ')]'; | ||||
| 			return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]'; | ||||
| 		} | ||||
|  | ||||
| 		let styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>'; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import { BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking } from 'react-native'; | ||||
| import { connect } from 'react-redux' | ||||
| import { uuid } from 'lib/uuid.js'; | ||||
| import { Log } from 'lib/log.js' | ||||
| import { Note } from 'lib/models/note.js' | ||||
| import { Resource } from 'lib/models/resource.js' | ||||
| @@ -19,6 +20,9 @@ import { dialogs } from 'lib/dialogs.js'; | ||||
| import { globalStyle, themeStyle } from 'lib/components/global-style.js'; | ||||
| import DialogBox from 'react-native-dialogbox'; | ||||
| import { NoteBodyViewer } from 'lib/components/note-body-viewer.js'; | ||||
| import RNFetchBlob from 'react-native-fetch-blob'; | ||||
| import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker'; | ||||
|  | ||||
|  | ||||
| class NoteScreenComponent extends BaseScreenComponent { | ||||
| 	 | ||||
| @@ -247,8 +251,42 @@ class NoteScreenComponent extends BaseScreenComponent { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	attachFile_onPress() { | ||||
| 	async pickDocument() { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			DocumentPicker.show({ filetype: [DocumentPickerUtil.images()] }, (error,res) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				resolve(res); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	async attachFile_onPress() { | ||||
| 		const res = await this.pickDocument(); | ||||
|  | ||||
| 		// res.uri, | ||||
| 		// res.type, // mime type | ||||
| 		// res.fileName, | ||||
| 		// res.fileSize | ||||
|  | ||||
| 		let resource = Resource.new(); | ||||
| 		resource.id = uuid.create(); | ||||
| 		resource.mime = res.type; | ||||
| 		resource.title = res.fileName ? res.fileName : _('Untitled'); | ||||
|  | ||||
| 		const targetPath = Resource.fullPath(resource); | ||||
| 		RNFetchBlob.fs.cp(res.uri, targetPath); | ||||
|  | ||||
| 		await Resource.save(resource, { isNew: true }); | ||||
|  | ||||
| 		const resourceTag = Resource.markdownTag(resource); | ||||
|  | ||||
| 			const newNote = Object.assign({}, this.state.note); | ||||
| 			newNote.body += "\n" + resourceTag; | ||||
| 			this.setState({ note: newNote }); | ||||
| 	} | ||||
|  | ||||
| 	toggleIsTodo_onPress() { | ||||
| @@ -279,7 +317,7 @@ class NoteScreenComponent extends BaseScreenComponent { | ||||
| 		const note = this.state.note; | ||||
|  | ||||
| 		return [ | ||||
| 			// { title: _('Attach file'), onPress: () => { this.attachFile_onPress(); } }, | ||||
| 			{ title: _('Attach file'), onPress: () => { this.attachFile_onPress(); } }, | ||||
| 			{ title: _('Delete note'), onPress: () => { this.deleteNote_onPress(); } }, | ||||
| 			{ title: note && !!note.is_todo ? _('Convert to regular note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } }, | ||||
| 			{ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } }, | ||||
|   | ||||
| @@ -126,11 +126,15 @@ class FileApiDriverOneDrive { | ||||
| 		return this.makeItem_(item); | ||||
| 	} | ||||
|  | ||||
| 	put(path, content) { | ||||
| 		let options = { | ||||
| 			headers: { 'Content-Type': 'text/plain' }, | ||||
| 		}; | ||||
| 		return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options); | ||||
| 	put(path, content, options = null) { | ||||
| 		if (!options) options = {}; | ||||
|  | ||||
| 		if (options.source == 'file') { | ||||
| 			return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, null, options); | ||||
| 		} else { | ||||
| 			options.headers = { 'Content-Type': 'text/plain' }; | ||||
| 			return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	delete(path) { | ||||
|   | ||||
| @@ -87,9 +87,9 @@ class FileApi { | ||||
| 		return this.driver_.get(this.fullPath_(path), options); | ||||
| 	} | ||||
|  | ||||
| 	put(path, content) { | ||||
| 	put(path, content, options = null) { | ||||
| 		this.logger().debug('put ' + this.fullPath_(path)); | ||||
| 		return this.driver_.put(this.fullPath_(path), content); | ||||
| 		return this.driver_.put(this.fullPath_(path), content, options); | ||||
| 	} | ||||
|  | ||||
| 	delete(path) { | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { BaseItem } from 'lib/models/base-item.js'; | ||||
| import { Setting } from 'lib/models/setting.js'; | ||||
| import { mime } from 'lib/mime-utils.js'; | ||||
| import { filename } from 'lib/path-utils.js'; | ||||
| import { FsDriverDummy } from 'lib/fs-driver-dummy.js'; | ||||
| import lodash  from 'lodash'; | ||||
|  | ||||
| class Resource extends BaseItem { | ||||
| @@ -15,6 +16,11 @@ class Resource extends BaseItem { | ||||
| 		return BaseModel.TYPE_RESOURCE; | ||||
| 	} | ||||
|  | ||||
| 	static isSupportedImageMimeType(type) { | ||||
| 		const imageMimeTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif"]; | ||||
| 		return imageMimeTypes.indexOf(type.toLowerCase()) >= 0; | ||||
| 	} | ||||
|  | ||||
| 	static fsDriver() { | ||||
| 		if (!Resource.fsDriver_) Resource.fsDriver_ = new FsDriverDummy(); | ||||
| 		return Resource.fsDriver_; | ||||
| @@ -32,6 +38,22 @@ class Resource extends BaseItem { | ||||
| 		return Setting.value('resourceDir') + '/' + resource.id + extension; | ||||
| 	} | ||||
|  | ||||
| 	static markdownTag(resource) { | ||||
| 		let tagAlt = resource.alt ? resource.alt : resource.title; | ||||
| 		if (!tagAlt) tagAlt = ''; | ||||
| 		let lines = []; | ||||
| 		if (Resource.isSupportedImageMimeType(resource.mime)) { | ||||
| 			lines.push(""); | ||||
| 		} else { | ||||
| 			lines.push("["); | ||||
| 			lines.push(tagAlt); | ||||
| 			lines.push("](:/" + resource.id + ")"); | ||||
| 		} | ||||
| 		return lines.join(''); | ||||
| 	} | ||||
|  | ||||
| 	static pathToId(path) { | ||||
| 		return filename(path); | ||||
| 	} | ||||
|   | ||||
| @@ -163,7 +163,9 @@ class OneDriveApi { | ||||
|  | ||||
| 			let response = null; | ||||
| 			try { | ||||
| 				if (options.target == 'string') { | ||||
| 				if (options.source == 'file' && (method == 'POST' || method == 'PUT')) { | ||||
| 					response = await shim.uploadBlob(url, options); | ||||
| 				} else if (options.target == 'string') { | ||||
| 					response = await shim.fetch(url, options); | ||||
| 				} else { // file | ||||
| 					response = await shim.fetchBlob(url, options); | ||||
|   | ||||
| @@ -7,7 +7,6 @@ function shimInit() { | ||||
|  | ||||
| 	shim.fetchBlob = async function(url, options) { | ||||
| 		if (!options || !options.path) throw new Error('fetchBlob: target file path is missing'); | ||||
| 		if (!options.method) options.method = 'GET'; | ||||
|  | ||||
| 		let headers = options.headers ? options.headers : {}; | ||||
| 		let method = options.method ? options.method : 'GET'; | ||||
| @@ -39,6 +38,29 @@ function shimInit() { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	shim.uploadBlob = async function(url, options) { | ||||
| 		if (!options || !options.path) throw new Error('uploadBlob: source file path is missing'); | ||||
|  | ||||
| 		const headers = options.headers ? options.headers : {}; | ||||
| 		const method = options.method ? options.method : 'POST'; | ||||
|  | ||||
| 		try { | ||||
| 			let response = await RNFetchBlob.fetch(method, url, headers, RNFetchBlob.wrap(options.path)); | ||||
|  | ||||
| 			// Returns an object that's roughtly compatible with a standard Response object | ||||
| 			return { | ||||
| 				ok: response.respInfo.status < 400, | ||||
| 				data: response.data, | ||||
| 				text: response.text, | ||||
| 				json: response.json, | ||||
| 				status: response.respInfo.status, | ||||
| 				headers: response.respInfo.headers, | ||||
| 			}; | ||||
| 		} catch (error) { | ||||
| 			throw new Error('uploadBlob: ' + method + ' ' + url + ': ' + error.toString()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	shim.readLocalFileBase64 = async function(path) { | ||||
| 		return RNFetchBlob.fs.readFile(path, 'base64') | ||||
| 	} | ||||
|   | ||||
| @@ -2,7 +2,7 @@ let shim = {}; | ||||
|  | ||||
| shim.isNode = () => { | ||||
| 	if (typeof process === 'undefined') return false; | ||||
| 	return process.title = 'node'; | ||||
| 	return process.title == 'node'; | ||||
| }; | ||||
|  | ||||
| shim.isReactNative = () => { | ||||
| @@ -14,5 +14,6 @@ shim.FormData = typeof FormData !== 'undefined' ? FormData : null; | ||||
| shim.fs = null; | ||||
| shim.FileApiDriverLocal = null; | ||||
| shim.readLocalFileBase64 = () => { throw new Error('Not implemented'); } | ||||
| shim.uploadBlob = () => { throw new Error('Not implemented'); } | ||||
|  | ||||
| export { shim }; | ||||
| @@ -7,6 +7,7 @@ import { sprintf } from 'sprintf-js'; | ||||
| import { time } from 'lib/time-utils.js'; | ||||
| import { Logger } from 'lib/logger.js' | ||||
| import { _ } from 'lib/locale.js'; | ||||
| import { shim } from 'lib/shim.js'; | ||||
| import moment from 'moment'; | ||||
|  | ||||
| class Synchronizer { | ||||
| @@ -235,15 +236,21 @@ class Synchronizer { | ||||
|  | ||||
| 					if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) { | ||||
| 						let remoteContentPath = this.resourceDirName_ + '/' + local.id; | ||||
| 						let resourceContent = ''; | ||||
| 						try { | ||||
| 							resourceContent = await Resource.content(local); | ||||
| 						} catch (error) { | ||||
| 							error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message; | ||||
| 							this.logger().error(error); | ||||
| 							this.progressReport_.errors.push(error); | ||||
| 						// TODO: handle node and mobile in the same way | ||||
| 						if (shim.isNode()) { | ||||
| 							let resourceContent = ''; | ||||
| 							try { | ||||
| 								resourceContent = await Resource.content(local); | ||||
| 							} catch (error) { | ||||
| 								error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message; | ||||
| 								this.logger().error(error); | ||||
| 								this.progressReport_.errors.push(error); | ||||
| 							} | ||||
| 							await this.api().put(remoteContentPath, resourceContent); | ||||
| 						} else { | ||||
| 							const localResourceContentPath = Resource.fullPath(local); | ||||
| 							await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' }); | ||||
| 						} | ||||
| 						await this.api().put(remoteContentPath, resourceContent); | ||||
| 					} | ||||
|  | ||||
| 					if (action == 'createRemote' || action == 'updateRemote') { | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
|     "react-native": "0.46.0", | ||||
|     "react-native-action-button": "^2.6.9", | ||||
|     "react-native-dialogbox": "^0.6.6", | ||||
|     "react-native-document-picker": "^2.0.0", | ||||
|     "react-native-fetch-blob": "^0.10.6", | ||||
|     "react-native-fs": "^2.3.3", | ||||
|     "react-native-popup-menu": "^0.7.4", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user