Skip to content

Commit ef2ffd4

Browse files
committed
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.
1 parent 5e3063a commit ef2ffd4

File tree

4 files changed

+102
-20
lines changed

4 files changed

+102
-20
lines changed

ElectronClient/app/gui/NoteText.jsx

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const MenuItem = bridge().MenuItem;
2020
const { shim } = require('lib/shim.js');
2121
const eventManager = require('../eventManager');
2222
const fs = require('fs-extra');
23+
const {clipboard} = require('electron')
24+
const md5 = require('md5');
25+
const mimeUtils = require('lib/mime-utils.js').mime;
2326

2427
require('brace/mode/markdown');
2528
// https://ace.c9.io/build/kitchen-sink.html
@@ -72,6 +75,62 @@ class NoteTextComponent extends React.Component {
7275
this.onAlarmChange_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
7376
this.onNoteTypeToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
7477
this.onTodoToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
78+
79+
this.onEditorPaste_ = async (event) => {
80+
const formats = clipboard.availableFormats();
81+
for (let i = 0; i < formats.length; i++) {
82+
const format = formats[i].toLowerCase();
83+
const formatType = format.split('/')[0]
84+
if (formatType === 'image') {
85+
event.preventDefault();
86+
87+
const image = clipboard.readImage();
88+
89+
const fileExt = mimeUtils.toFileExtension(format);
90+
const filePath = Setting.value('tempDir') + '/' + md5(Date.now()) + '.' + fileExt;
91+
92+
await shim.writeImageToFile(image, format, filePath);
93+
await this.commandAttachFile([filePath]);
94+
await shim.fsDriver().remove(filePath);
95+
}
96+
}
97+
}
98+
99+
this.onDrop_ = async (event) => {
100+
const files = event.dataTransfer.files;
101+
if (!files || !files.length) return;
102+
103+
const filesToAttach = [];
104+
105+
for (let i = 0; i < files.length; i++) {
106+
const file = files[i];
107+
if (!file.path) continue;
108+
filesToAttach.push(file.path);
109+
}
110+
111+
await this.commandAttachFile(filesToAttach);
112+
}
113+
}
114+
115+
cursorPosition() {
116+
if (!this.editor_ || !this.editor_.editor || !this.state.note || !this.state.note.body) return 0;
117+
118+
const cursorPos = this.editor_.editor.getCursorPosition();
119+
const noteLines = this.state.note.body.split('\n');
120+
121+
let pos = 0;
122+
for (let i = 0; i < noteLines.length; i++) {
123+
if (i > 0) pos++; // Need to add the newline that's been removed in the split() call above
124+
125+
if (i === cursorPos.row) {
126+
pos += cursorPos.column;
127+
break;
128+
} else {
129+
pos += noteLines[i].length;
130+
}
131+
}
132+
133+
return pos;
75134
}
76135

77136
mdToHtml() {
@@ -421,6 +480,7 @@ class NoteTextComponent extends React.Component {
421480

422481
if (this.editor_) {
423482
this.editor_.editor.renderer.off('afterRender', this.onAfterEditorRender_);
483+
document.querySelector('#note-editor').removeEventListener('paste', this.onEditorPaste_, true);
424484
}
425485

426486
this.editor_ = element;
@@ -446,6 +506,8 @@ class NoteTextComponent extends React.Component {
446506
throw new Error('HACK: Overriding Ace Editor shortcut: ' + k);
447507
});
448508
}
509+
510+
document.querySelector('#note-editor').addEventListener('paste', this.onEditorPaste_, true);
449511
}
450512
}
451513

@@ -516,20 +578,24 @@ class NoteTextComponent extends React.Component {
516578
}
517579
}
518580

519-
async commandAttachFile() {
520-
const filePaths = bridge().showOpenDialog({
521-
properties: ['openFile', 'createDirectory', 'multiSelections'],
522-
});
523-
if (!filePaths || !filePaths.length) return;
581+
async commandAttachFile(filePaths = null) {
582+
if (!filePaths) {
583+
filePaths = bridge().showOpenDialog({
584+
properties: ['openFile', 'createDirectory', 'multiSelections'],
585+
});
586+
if (!filePaths || !filePaths.length) return;
587+
}
524588

525589
await this.saveIfNeeded(true);
526590
let note = await Note.load(this.state.note.id);
527591

592+
const position = this.cursorPosition();
593+
528594
for (let i = 0; i < filePaths.length; i++) {
529595
const filePath = filePaths[i];
530596
try {
531597
reg.logger().info('Attaching ' + filePath);
532-
note = await shim.attachFileToNote(note, filePath);
598+
note = await shim.attachFileToNote(note, filePath, position);
533599
reg.logger().info('File was attached.');
534600
this.setState({
535601
note: Object.assign({}, note),
@@ -801,7 +867,7 @@ class NoteTextComponent extends React.Component {
801867
/>
802868

803869
return (
804-
<div style={rootStyle}>
870+
<div style={rootStyle} onDrop={this.onDrop_}>
805871
<div style={titleBarStyle}>
806872
{ titleEditor }
807873
{ titleBarDate }

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ For a more technical description, mostly relevant for development or to review t
183183

184184
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.
185185

186+
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.
187+
186188
Resources that are not attached to any note will be automatically deleted after a day or two.
187189

188190
**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.

ReactNativeClient/lib/shim-init-node.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ function shimInit() {
3535
return locale;
3636
}
3737

38+
// For Electron only
39+
shim.writeImageToFile = async function(nativeImage, mime, targetPath) {
40+
let buffer = null;
41+
42+
mime = mime.toLowerCase();
43+
44+
if (mime === 'image/png') {
45+
buffer = nativeImage.toPNG();
46+
} else if (mime === 'image/jpg' || mime === 'image/jpeg') {
47+
buffer = nativeImage.toJPEG(90);
48+
}
49+
50+
if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath);
51+
52+
await shim.fsDriver().writeFile(targetPath, buffer, 'buffer');
53+
}
54+
3855
const resizeImage_ = async function(filePath, targetPath, mime) {
3956
if (shim.isElectron()) { // For Electron
4057
const nativeImage = require('electron').nativeImage;
@@ -58,17 +75,7 @@ function shimInit() {
5875

5976
image = image.resize(options);
6077

61-
let buffer = null;
62-
63-
if (mime === 'image/png') {
64-
buffer = image.toPNG();
65-
} else if (mime === 'image/jpg' || mime === 'image/jpeg') {
66-
buffer = image.toJPEG(90);
67-
}
68-
69-
if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath);
70-
71-
await shim.fsDriver().writeFile(targetPath, buffer, 'buffer');
78+
await shim.writeImageToFile(image, mime, targetPath);
7279
} else { // For the CLI tool
7380
const sharp = require('sharp');
7481
const Resource = require('lib/models/Resource.js');
@@ -89,7 +96,7 @@ function shimInit() {
8996
}
9097
}
9198

92-
shim.attachFileToNote = async function(note, filePath) {
99+
shim.attachFileToNote = async function(note, filePath, position = null) {
93100
const Resource = require('lib/models/Resource.js');
94101
const { uuid } = require('lib/uuid.js');
95102
const { basename, fileExtension, safeFileExtension } = require('lib/path-utils.js');
@@ -120,8 +127,14 @@ function shimInit() {
120127
await Resource.save(resource, { isNew: true });
121128

122129
const newBody = [];
123-
if (note.body) newBody.push(note.body);
130+
131+
if (position === null) {
132+
position = note.body ? note.body.length : 0;
133+
}
134+
135+
if (note.body && position) newBody.push(note.body.substr(0, position));
124136
newBody.push(Resource.markdownTag(resource));
137+
newBody.push(note.body.substr(position));
125138

126139
const newNote = Object.assign({}, note, {
127140
body: newBody.join('\n\n'),

docs/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ <h1 id="encryption">Encryption</h1>
409409
<p>For a more technical description, mostly relevant for development or to review the method being used, please see the <a href="https://joplin.cozic.net/spec">Encryption specification</a>.</p>
410410
<h1 id="attachments-resources">Attachments / Resources</h1>
411411
<p>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.</p>
412+
<p>On the <strong>desktop application</strong>, images can be attached either by clicking on &quot;Attach file&quot; or by pasting (with Ctrl+V) an image directly in the editor, or by drag and dropping an image.</p>
412413
<p>Resources that are not attached to any note will be automatically deleted after a day or two.</p>
413414
<p><strong>Important:</strong> 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.</p>
414415
<h1 id="notifications">Notifications</h1>

0 commit comments

Comments
 (0)