1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Attach resource from mobile

This commit is contained in:
Laurent Cozic 2017-08-01 23:40:14 +02:00
parent 7fe70696bb
commit 19266206f4
14 changed files with 131 additions and 25 deletions

View File

@ -164,6 +164,8 @@ function isImageMimeType(m) {
} }
function addResourceTag(lines, resource, alt = "") { function addResourceTag(lines, resource, alt = "") {
// TODO: refactor to use Resource.markdownTag
let tagAlt = alt == "" ? resource.alt : alt; let tagAlt = alt == "" ? resource.alt : alt;
if (!tagAlt) tagAlt = ''; if (!tagAlt) tagAlt = '';
if (isImageMimeType(resource.mime)) { if (isImageMimeType(resource.mime)) {

View File

@ -143,6 +143,7 @@ dependencies {
compile "com.facebook.react:react-native:+" // From node_modules compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-sqlite-storage') compile project(':react-native-sqlite-storage')
compile project(':react-native-fetch-blob') compile project(':react-native-fetch-blob')
compile project(':react-native-document-picker')
} }
// Run this once to be able to run the application with BUCK // Run this once to be able to run the application with BUCK

View File

@ -10,6 +10,7 @@ import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader; import com.facebook.soloader.SoLoader;
import org.pgsqlite.SQLitePluginPackage; import org.pgsqlite.SQLitePluginPackage;
import com.RNFetchBlob.RNFetchBlobPackage; import com.RNFetchBlob.RNFetchBlobPackage;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -28,7 +29,8 @@ public class MainApplication extends Application implements ReactApplication {
new SQLitePluginPackage(), new SQLitePluginPackage(),
new MainReactPackage(), new MainReactPackage(),
new RNFSPackage(), new RNFSPackage(),
new RNFetchBlobPackage() new RNFetchBlobPackage(),
new ReactNativeDocumentPicker()
); );
} }
}; };

View File

@ -9,3 +9,6 @@ project(':react-native-sqlite-storage').projectDir = new File(rootProject.projec
include ':react-native-fetch-blob' 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')

View File

@ -126,13 +126,14 @@ class NoteBodyViewer extends Component {
} }
const r = this.state.resources[resourceId]; 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; 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 output;
} }
return '[Image: ' + htmlentities(r.title) + '(' + htmlentities(r.mime) + ')]'; return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]';
} }
let styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>'; let styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking } from 'react-native'; import { BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking } from 'react-native';
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { uuid } from 'lib/uuid.js';
import { Log } from 'lib/log.js' import { Log } from 'lib/log.js'
import { Note } from 'lib/models/note.js' import { Note } from 'lib/models/note.js'
import { Resource } from 'lib/models/resource.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 { globalStyle, themeStyle } from 'lib/components/global-style.js';
import DialogBox from 'react-native-dialogbox'; import DialogBox from 'react-native-dialogbox';
import { NoteBodyViewer } from 'lib/components/note-body-viewer.js'; 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 { 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() { toggleIsTodo_onPress() {
@ -279,7 +317,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const note = this.state.note; const note = this.state.note;
return [ return [
// { title: _('Attach file'), onPress: () => { this.attachFile_onPress(); } }, { title: _('Attach file'), onPress: () => { this.attachFile_onPress(); } },
{ title: _('Delete note'), onPress: () => { this.deleteNote_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: 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(); } }, { title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } },

View File

@ -126,12 +126,16 @@ class FileApiDriverOneDrive {
return this.makeItem_(item); return this.makeItem_(item);
} }
put(path, content) { put(path, content, options = null) {
let options = { if (!options) options = {};
headers: { 'Content-Type': 'text/plain' },
}; 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); return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options);
} }
}
delete(path) { delete(path) {
return this.api_.exec('DELETE', this.makePath_(path)); return this.api_.exec('DELETE', this.makePath_(path));

View File

@ -87,9 +87,9 @@ class FileApi {
return this.driver_.get(this.fullPath_(path), options); return this.driver_.get(this.fullPath_(path), options);
} }
put(path, content) { put(path, content, options = null) {
this.logger().debug('put ' + this.fullPath_(path)); 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) { delete(path) {

View File

@ -3,6 +3,7 @@ import { BaseItem } from 'lib/models/base-item.js';
import { Setting } from 'lib/models/setting.js'; import { Setting } from 'lib/models/setting.js';
import { mime } from 'lib/mime-utils.js'; import { mime } from 'lib/mime-utils.js';
import { filename } from 'lib/path-utils.js'; import { filename } from 'lib/path-utils.js';
import { FsDriverDummy } from 'lib/fs-driver-dummy.js';
import lodash from 'lodash'; import lodash from 'lodash';
class Resource extends BaseItem { class Resource extends BaseItem {
@ -15,6 +16,11 @@ class Resource extends BaseItem {
return BaseModel.TYPE_RESOURCE; 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() { static fsDriver() {
if (!Resource.fsDriver_) Resource.fsDriver_ = new FsDriverDummy(); if (!Resource.fsDriver_) Resource.fsDriver_ = new FsDriverDummy();
return Resource.fsDriver_; return Resource.fsDriver_;
@ -32,6 +38,22 @@ class Resource extends BaseItem {
return Setting.value('resourceDir') + '/' + resource.id + extension; 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("![");
lines.push(tagAlt);
lines.push("](:/" + resource.id + ")");
} else {
lines.push("[");
lines.push(tagAlt);
lines.push("](:/" + resource.id + ")");
}
return lines.join('');
}
static pathToId(path) { static pathToId(path) {
return filename(path); return filename(path);
} }

View File

@ -163,7 +163,9 @@ class OneDriveApi {
let response = null; let response = null;
try { 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); response = await shim.fetch(url, options);
} else { // file } else { // file
response = await shim.fetchBlob(url, options); response = await shim.fetchBlob(url, options);

View File

@ -7,7 +7,6 @@ function shimInit() {
shim.fetchBlob = async function(url, options) { shim.fetchBlob = async function(url, options) {
if (!options || !options.path) throw new Error('fetchBlob: target file path is missing'); 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 headers = options.headers ? options.headers : {};
let method = options.method ? options.method : 'GET'; 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) { shim.readLocalFileBase64 = async function(path) {
return RNFetchBlob.fs.readFile(path, 'base64') return RNFetchBlob.fs.readFile(path, 'base64')
} }

View File

@ -2,7 +2,7 @@ let shim = {};
shim.isNode = () => { shim.isNode = () => {
if (typeof process === 'undefined') return false; if (typeof process === 'undefined') return false;
return process.title = 'node'; return process.title == 'node';
}; };
shim.isReactNative = () => { shim.isReactNative = () => {
@ -14,5 +14,6 @@ shim.FormData = typeof FormData !== 'undefined' ? FormData : null;
shim.fs = null; shim.fs = null;
shim.FileApiDriverLocal = null; shim.FileApiDriverLocal = null;
shim.readLocalFileBase64 = () => { throw new Error('Not implemented'); } shim.readLocalFileBase64 = () => { throw new Error('Not implemented'); }
shim.uploadBlob = () => { throw new Error('Not implemented'); }
export { shim }; export { shim };

View File

@ -7,6 +7,7 @@ import { sprintf } from 'sprintf-js';
import { time } from 'lib/time-utils.js'; import { time } from 'lib/time-utils.js';
import { Logger } from 'lib/logger.js' import { Logger } from 'lib/logger.js'
import { _ } from 'lib/locale.js'; import { _ } from 'lib/locale.js';
import { shim } from 'lib/shim.js';
import moment from 'moment'; import moment from 'moment';
class Synchronizer { class Synchronizer {
@ -235,6 +236,8 @@ class Synchronizer {
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) { if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) {
let remoteContentPath = this.resourceDirName_ + '/' + local.id; let remoteContentPath = this.resourceDirName_ + '/' + local.id;
// TODO: handle node and mobile in the same way
if (shim.isNode()) {
let resourceContent = ''; let resourceContent = '';
try { try {
resourceContent = await Resource.content(local); resourceContent = await Resource.content(local);
@ -244,6 +247,10 @@ class Synchronizer {
this.progressReport_.errors.push(error); this.progressReport_.errors.push(error);
} }
await this.api().put(remoteContentPath, resourceContent); await this.api().put(remoteContentPath, resourceContent);
} else {
const localResourceContentPath = Resource.fullPath(local);
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
}
} }
if (action == 'createRemote' || action == 'updateRemote') { if (action == 'createRemote' || action == 'updateRemote') {

View File

@ -17,6 +17,7 @@
"react-native": "0.46.0", "react-native": "0.46.0",
"react-native-action-button": "^2.6.9", "react-native-action-button": "^2.6.9",
"react-native-dialogbox": "^0.6.6", "react-native-dialogbox": "^0.6.6",
"react-native-document-picker": "^2.0.0",
"react-native-fetch-blob": "^0.10.6", "react-native-fetch-blob": "^0.10.6",
"react-native-fs": "^2.3.3", "react-native-fs": "^2.3.3",
"react-native-popup-menu": "^0.7.4", "react-native-popup-menu": "^0.7.4",