From ef2ffd4e52957d3131604afeb4f047870013407b Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 10 May 2018 10:45:44 +0100 Subject: [PATCH] Electron: Resolves #200, Resolves #416: Allow attaching images by pasting them in. Allow attaching files by drag and dropping them. Insert attachement at cursor position. --- ElectronClient/app/gui/NoteText.jsx | 80 ++++++++++++++++++++++--- README.md | 2 + ReactNativeClient/lib/shim-init-node.js | 39 ++++++++---- docs/index.html | 1 + 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index 66160fe77..ee0aa504c 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -20,6 +20,9 @@ const MenuItem = bridge().MenuItem; const { shim } = require('lib/shim.js'); const eventManager = require('../eventManager'); const fs = require('fs-extra'); +const {clipboard} = require('electron') +const md5 = require('md5'); +const mimeUtils = require('lib/mime-utils.js').mime; require('brace/mode/markdown'); // https://ace.c9.io/build/kitchen-sink.html @@ -72,6 +75,62 @@ class NoteTextComponent extends React.Component { this.onAlarmChange_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); } this.onNoteTypeToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); } this.onTodoToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); } + + this.onEditorPaste_ = async (event) => { + const formats = clipboard.availableFormats(); + for (let i = 0; i < formats.length; i++) { + const format = formats[i].toLowerCase(); + const formatType = format.split('/')[0] + if (formatType === 'image') { + event.preventDefault(); + + const image = clipboard.readImage(); + + const fileExt = mimeUtils.toFileExtension(format); + const filePath = Setting.value('tempDir') + '/' + md5(Date.now()) + '.' + fileExt; + + await shim.writeImageToFile(image, format, filePath); + await this.commandAttachFile([filePath]); + await shim.fsDriver().remove(filePath); + } + } + } + + this.onDrop_ = async (event) => { + const files = event.dataTransfer.files; + if (!files || !files.length) return; + + const filesToAttach = []; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (!file.path) continue; + filesToAttach.push(file.path); + } + + await this.commandAttachFile(filesToAttach); + } + } + + cursorPosition() { + if (!this.editor_ || !this.editor_.editor || !this.state.note || !this.state.note.body) return 0; + + const cursorPos = this.editor_.editor.getCursorPosition(); + const noteLines = this.state.note.body.split('\n'); + + let pos = 0; + for (let i = 0; i < noteLines.length; i++) { + if (i > 0) pos++; // Need to add the newline that's been removed in the split() call above + + if (i === cursorPos.row) { + pos += cursorPos.column; + break; + } else { + pos += noteLines[i].length; + } + } + + return pos; } mdToHtml() { @@ -421,6 +480,7 @@ class NoteTextComponent extends React.Component { if (this.editor_) { this.editor_.editor.renderer.off('afterRender', this.onAfterEditorRender_); + document.querySelector('#note-editor').removeEventListener('paste', this.onEditorPaste_, true); } this.editor_ = element; @@ -446,6 +506,8 @@ class NoteTextComponent extends React.Component { throw new Error('HACK: Overriding Ace Editor shortcut: ' + k); }); } + + document.querySelector('#note-editor').addEventListener('paste', this.onEditorPaste_, true); } } @@ -516,20 +578,24 @@ class NoteTextComponent extends React.Component { } } - async commandAttachFile() { - const filePaths = bridge().showOpenDialog({ - properties: ['openFile', 'createDirectory', 'multiSelections'], - }); - if (!filePaths || !filePaths.length) return; + async commandAttachFile(filePaths = null) { + if (!filePaths) { + filePaths = bridge().showOpenDialog({ + properties: ['openFile', 'createDirectory', 'multiSelections'], + }); + if (!filePaths || !filePaths.length) return; + } await this.saveIfNeeded(true); let note = await Note.load(this.state.note.id); + const position = this.cursorPosition(); + for (let i = 0; i < filePaths.length; i++) { const filePath = filePaths[i]; try { reg.logger().info('Attaching ' + filePath); - note = await shim.attachFileToNote(note, filePath); + note = await shim.attachFileToNote(note, filePath, position); reg.logger().info('File was attached.'); this.setState({ note: Object.assign({}, note), @@ -801,7 +867,7 @@ class NoteTextComponent extends React.Component { /> return ( -
+
{ titleEditor } { titleBarDate } diff --git a/README.md b/README.md index 9e9bbc967..25be3799c 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,8 @@ For a more technical description, mostly relevant for development or to review t Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application. +On the **desktop application**, images can be attached either by clicking on "Attach file" or by pasting (with Ctrl+V) an image directly in the editor, or by drag and dropping an image. + Resources that are not attached to any note will be automatically deleted after a day or two. **Important:** Resources larger than 10 MB are not currently supported on mobile. They will crash the application when synchronising so it is recommended not to attach such resources at the moment. The issue is being looked at. diff --git a/ReactNativeClient/lib/shim-init-node.js b/ReactNativeClient/lib/shim-init-node.js index c9f155f94..1e9ecd664 100644 --- a/ReactNativeClient/lib/shim-init-node.js +++ b/ReactNativeClient/lib/shim-init-node.js @@ -35,6 +35,23 @@ function shimInit() { return locale; } + // For Electron only + shim.writeImageToFile = async function(nativeImage, mime, targetPath) { + let buffer = null; + + mime = mime.toLowerCase(); + + if (mime === 'image/png') { + buffer = nativeImage.toPNG(); + } else if (mime === 'image/jpg' || mime === 'image/jpeg') { + buffer = nativeImage.toJPEG(90); + } + + if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath); + + await shim.fsDriver().writeFile(targetPath, buffer, 'buffer'); + } + const resizeImage_ = async function(filePath, targetPath, mime) { if (shim.isElectron()) { // For Electron const nativeImage = require('electron').nativeImage; @@ -58,17 +75,7 @@ function shimInit() { image = image.resize(options); - let buffer = null; - - if (mime === 'image/png') { - buffer = image.toPNG(); - } else if (mime === 'image/jpg' || mime === 'image/jpeg') { - buffer = image.toJPEG(90); - } - - if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath); - - await shim.fsDriver().writeFile(targetPath, buffer, 'buffer'); + await shim.writeImageToFile(image, mime, targetPath); } else { // For the CLI tool const sharp = require('sharp'); const Resource = require('lib/models/Resource.js'); @@ -89,7 +96,7 @@ function shimInit() { } } - shim.attachFileToNote = async function(note, filePath) { + shim.attachFileToNote = async function(note, filePath, position = null) { const Resource = require('lib/models/Resource.js'); const { uuid } = require('lib/uuid.js'); const { basename, fileExtension, safeFileExtension } = require('lib/path-utils.js'); @@ -120,8 +127,14 @@ function shimInit() { await Resource.save(resource, { isNew: true }); const newBody = []; - if (note.body) newBody.push(note.body); + + if (position === null) { + position = note.body ? note.body.length : 0; + } + + if (note.body && position) newBody.push(note.body.substr(0, position)); newBody.push(Resource.markdownTag(resource)); + newBody.push(note.body.substr(position)); const newNote = Object.assign({}, note, { body: newBody.join('\n\n'), diff --git a/docs/index.html b/docs/index.html index c08337e9b..5c7f460c4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -409,6 +409,7 @@

For a more technical description, mostly relevant for development or to review the method being used, please see the Encryption specification.

Attachments / Resources

Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.

+

On the desktop application, images can be attached either by clicking on "Attach file" or by pasting (with Ctrl+V) an image directly in the editor, or by drag and dropping an image.

Resources that are not attached to any note will be automatically deleted after a day or two.

Important: Resources larger than 10 MB are not currently supported on mobile. They will crash the application when synchronising so it is recommended not to attach such resources at the moment. The issue is being looked at.

Notifications