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 = "") {
// TODO: refactor to use Resource.markdownTag
let tagAlt = alt == "" ? resource.alt : alt;
if (!tagAlt) tagAlt = '';
if (isImageMimeType(resource.mime)) {

View File

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

View File

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

View File

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

View File

@ -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>';

View File

@ -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(); } },

View File

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

View File

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

View File

@ -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("![");
lines.push(tagAlt);
lines.push("](:/" + resource.id + ")");
} else {
lines.push("[");
lines.push(tagAlt);
lines.push("](:/" + resource.id + ")");
}
return lines.join('');
}
static pathToId(path) {
return filename(path);
}

View File

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

View File

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

View File

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

View File

@ -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') {

View File

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