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:
parent
7fe70696bb
commit
19266206f4
@ -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("![");
|
||||
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);
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user