mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-27 08:21:03 +02:00
Clipper: Support clipping screenshots
This commit is contained in:
parent
4640d6d6e3
commit
264ee4f319
@ -8,10 +8,10 @@ if (typeof browser !== 'undefined') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function browserCaptureVisibleTabs(windowId, options) {
|
async function browserCaptureVisibleTabs(windowId, options) {
|
||||||
return new Promise((resolve, reject) => {
|
if (browserSupportsPromises_) return browser_.tabs.captureVisibleTab(windowId, { format: 'jpeg' });
|
||||||
if (browserSupportsPromises_) return browser_.tabs.captureVisibleTab(null, { format: 'jpeg' });
|
|
||||||
|
|
||||||
browser_.tabs.captureVisibleTab(null, { format: 'jpeg' }, (image) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
browser_.tabs.captureVisibleTab(windowId, { format: 'jpeg' }, (image) => {
|
||||||
resolve(image);
|
resolve(image);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -23,9 +23,9 @@ chrome.runtime.onInstalled.addListener(function() {
|
|||||||
|
|
||||||
browser_.runtime.onMessage.addListener((command) => {
|
browser_.runtime.onMessage.addListener((command) => {
|
||||||
if (command.name === 'screenshotArea') {
|
if (command.name === 'screenshotArea') {
|
||||||
browserCaptureVisibleTabs(null, { format: 'jpeg' }).then((image) => {
|
browserCaptureVisibleTabs(null, { format: 'jpeg' }).then((imageDataUrl) => {
|
||||||
content = Object.assign({}, command.content);
|
content = Object.assign({}, command.content);
|
||||||
content.imageBase64 = image;
|
content.imageDataUrl = imageDataUrl;
|
||||||
|
|
||||||
fetch(command.apiBaseUrl + "/notes", {
|
fetch(command.apiBaseUrl + "/notes", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -112,7 +112,7 @@
|
|||||||
} else if (command.name === 'screenshot') {
|
} else if (command.name === 'screenshot') {
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.style.opacity = '0.5';
|
overlay.style.opacity = '0.4';
|
||||||
overlay.style.background = 'black';
|
overlay.style.background = 'black';
|
||||||
overlay.style.width = '100%';
|
overlay.style.width = '100%';
|
||||||
overlay.style.height = '100%';
|
overlay.style.height = '100%';
|
||||||
@ -123,9 +123,30 @@
|
|||||||
|
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
const messageComp = document.createElement('div');
|
||||||
|
|
||||||
|
const messageCompWidth = 300;
|
||||||
|
messageComp.style.position = 'fixed'
|
||||||
|
messageComp.style.opacity = '0.9'
|
||||||
|
messageComp.style.width = messageCompWidth + 'px'
|
||||||
|
messageComp.style.maxWidth = messageCompWidth + 'px'
|
||||||
|
messageComp.style.border = '1px solid black'
|
||||||
|
messageComp.style.background = 'white'
|
||||||
|
messageComp.style.top = '10px'
|
||||||
|
messageComp.style.textAlign = 'center';
|
||||||
|
messageComp.style.padding = '6px'
|
||||||
|
messageComp.style.left = Math.round(document.body.clientWidth / 2 - messageCompWidth / 2) + 'px'
|
||||||
|
messageComp.style.zIndex = overlay.style.zIndex + 1
|
||||||
|
|
||||||
|
messageComp.textContent = 'Drag and release to capture a screenshot';
|
||||||
|
|
||||||
|
document.body.appendChild(messageComp);
|
||||||
|
|
||||||
const selection = document.createElement('div');
|
const selection = document.createElement('div');
|
||||||
selection.style.opacity = '0.5';
|
selection.style.opacity = '0.4';
|
||||||
selection.style.background = 'blue';
|
selection.style.border = '1px solid red';
|
||||||
|
selection.style.background = 'white';
|
||||||
|
selection.style.border = '2px solid black';
|
||||||
selection.style.zIndex = overlay.style.zIndex - 1;
|
selection.style.zIndex = overlay.style.zIndex - 1;
|
||||||
selection.style.top = 0;
|
selection.style.top = 0;
|
||||||
selection.style.left = 0;
|
selection.style.left = 0;
|
||||||
@ -138,21 +159,21 @@
|
|||||||
let selectionArea = {};
|
let selectionArea = {};
|
||||||
|
|
||||||
function updateSelection() {
|
function updateSelection() {
|
||||||
selection.style.left = selectionArea.x;
|
selection.style.left = selectionArea.x + 'px';
|
||||||
selection.style.top = selectionArea.y;
|
selection.style.top = selectionArea.y + 'px';
|
||||||
selection.style.width = selectionArea.width;
|
selection.style.width = selectionArea.width + 'px';
|
||||||
selection.style.height = selectionArea.height;
|
selection.style.height = selectionArea.height + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSelectionSizeFromMouse(event) {
|
function setSelectionSizeFromMouse(event) {
|
||||||
selectionArea.width = Math.max(1, event.pageX - draggingStartPos.x);
|
selectionArea.width = Math.max(1, event.clientX - draggingStartPos.x);
|
||||||
selectionArea.height = Math.max(1, event.pageY - draggingStartPos.y);
|
selectionArea.height = Math.max(1, event.clientY - draggingStartPos.y);
|
||||||
updateSelection();
|
updateSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selection_mouseDown(event) {
|
function selection_mouseDown(event) {
|
||||||
selectionArea = { x: event.pageX - document.body.scrollLeft, y: event.pageY - document.body.scrollTop, width: 0, height: 0 }
|
selectionArea = { x: event.clientX, y: event.clientY, width: 0, height: 0 }
|
||||||
draggingStartPos = { x: event.pageX, y: event.pageY };
|
draggingStartPos = { x: event.clientX, y: event.clientY };
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
updateSelection();
|
updateSelection();
|
||||||
}
|
}
|
||||||
@ -173,10 +194,14 @@
|
|||||||
|
|
||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
document.body.removeChild(selection);
|
document.body.removeChild(selection);
|
||||||
|
document.body.removeChild(messageComp);
|
||||||
|
|
||||||
|
if (!selectionArea || !selectionArea.width || !selectionArea.height) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
const content = {
|
const content = {
|
||||||
title: pageTitle(),
|
title: pageTitle(),
|
||||||
area: selectionArea,
|
cropRect: selectionArea,
|
||||||
url: location.origin + location.pathname,
|
url: location.origin + location.pathname,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -185,6 +210,7 @@
|
|||||||
content: content,
|
content: content,
|
||||||
apiBaseUrl: command.apiBaseUrl,
|
apiBaseUrl: command.apiBaseUrl,
|
||||||
});
|
});
|
||||||
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay.addEventListener('mousedown', selection_mouseDown);
|
overlay.addEventListener('mousedown', selection_mouseDown);
|
||||||
|
@ -77,17 +77,26 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App .Preview .Body {
|
.App .Preview .BodyWrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App .Preview .Body {
|
||||||
|
/*flex: 1;*/
|
||||||
font-size: .5em;
|
font-size: .5em;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
flex-shrink: 1;
|
/*flex-shrink: 1;*/
|
||||||
min-width: auto;
|
/*min-width: auto;*/
|
||||||
padding: 10px;
|
/*padding: 10px;*/
|
||||||
margin-bottom: 10px;
|
/*margin-bottom: 10px;*/
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App .Preview .Confirm {
|
.App .Preview .Confirm {
|
||||||
|
@ -43,6 +43,8 @@ class AppComponent extends Component {
|
|||||||
name: 'screenshot',
|
name: 'screenshot',
|
||||||
apiBaseUrl: 'http://127.0.0.1:9967',
|
apiBaseUrl: 'http://127.0.0.1:9967',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadContentScripts() {
|
async loadContentScripts() {
|
||||||
@ -91,7 +93,9 @@ class AppComponent extends Component {
|
|||||||
previewComponent = (
|
previewComponent = (
|
||||||
<div className="Preview">
|
<div className="Preview">
|
||||||
<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/>
|
<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/>
|
||||||
|
<div className={"BodyWrapper"}>
|
||||||
<div className={"Body"} dangerouslySetInnerHTML={{__html: content.bodyHtml}}></div>
|
<div className={"Body"} dangerouslySetInnerHTML={{__html: content.bodyHtml}}></div>
|
||||||
|
</div>
|
||||||
<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a>
|
<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ const { fileExtension, safeFileExtension, safeFilename, filename } = require('li
|
|||||||
const HtmlToMd = require('lib/HtmlToMd');
|
const HtmlToMd = require('lib/HtmlToMd');
|
||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const markdownUtils = require('lib/markdownUtils');
|
const markdownUtils = require('lib/markdownUtils');
|
||||||
|
const mimeUtils = require('lib/mime-utils.js').mime;
|
||||||
|
|
||||||
class ClipperServer {
|
class ClipperServer {
|
||||||
|
|
||||||
@ -59,6 +60,19 @@ class ClipperServer {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note must have been saved first
|
||||||
|
async attachImageFromDataUrl_(note, imageDataUrl, cropRect) {
|
||||||
|
const tempDir = Setting.value('tempDir');
|
||||||
|
const mime = mimeUtils.fromDataUrl(imageDataUrl);
|
||||||
|
let ext = mimeUtils.toFileExtension(mime) || '';
|
||||||
|
if (ext) ext = '.' + ext;
|
||||||
|
const tempFilePath = tempDir + '/' + md5(Math.random() + '_' + Date.now()) + ext;
|
||||||
|
const imageConvOptions = {};
|
||||||
|
if (cropRect) imageConvOptions.cropRect = cropRect;
|
||||||
|
await shim.imageFromDataUrl(imageDataUrl, tempFilePath, imageConvOptions);
|
||||||
|
return await shim.attachFileToNote(note, tempFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
async downloadImage_(url) {
|
async downloadImage_(url) {
|
||||||
const tempDir = Setting.value('tempDir');
|
const tempDir = Setting.value('tempDir');
|
||||||
const name = filename(url);
|
const name = filename(url);
|
||||||
@ -189,6 +203,11 @@ class ClipperServer {
|
|||||||
note.body = this.replaceImageUrlsByResources_(note.body, result);
|
note.body = this.replaceImageUrlsByResources_(note.body, result);
|
||||||
|
|
||||||
note = await Note.save(note);
|
note = await Note.save(note);
|
||||||
|
|
||||||
|
if (requestNote.imageDataUrl) {
|
||||||
|
await this.attachImageFromDataUrl_(note, requestNote.imageDataUrl, requestNote.cropRect);
|
||||||
|
}
|
||||||
|
|
||||||
this.logger().info('Request (' + requestId + '): Created note ' + note.id);
|
this.logger().info('Request (' + requestId + '): Created note ' + note.id);
|
||||||
return writeResponseJson(200, note);
|
return writeResponseJson(200, note);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -29,6 +29,17 @@ const mime = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fromDataUrl(dataUrl) {
|
||||||
|
// Example: data:image/jpeg;base64,/9j/4AAQSkZJR.....
|
||||||
|
const defaultMime = 'text/plain';
|
||||||
|
let p = dataUrl.substr(0, dataUrl.indexOf(',')).split(';');
|
||||||
|
let s = p[0];
|
||||||
|
s = s.split(':');
|
||||||
|
if (s.length <= 1) return defaultMime;
|
||||||
|
s = s[1];
|
||||||
|
return s.indexOf('/') >= 0 ? s : defaultMime; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { mime };
|
module.exports = { mime };
|
@ -5,6 +5,7 @@ const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
|||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
|
const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
|
||||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||||
|
const mimeUtils = require('lib/mime-utils.js').mime;
|
||||||
const urlValidator = require('valid-url');
|
const urlValidator = require('valid-url');
|
||||||
|
|
||||||
function shimInit() {
|
function shimInit() {
|
||||||
@ -35,8 +36,8 @@ function shimInit() {
|
|||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Electron only
|
|
||||||
shim.writeImageToFile = async function(nativeImage, mime, targetPath) {
|
shim.writeImageToFile = async function(nativeImage, mime, targetPath) {
|
||||||
|
if (shim.isElectron()) { // For Electron
|
||||||
let buffer = null;
|
let buffer = null;
|
||||||
|
|
||||||
mime = mime.toLowerCase();
|
mime = mime.toLowerCase();
|
||||||
@ -47,9 +48,12 @@ function shimInit() {
|
|||||||
buffer = nativeImage.toJPEG(90);
|
buffer = nativeImage.toJPEG(90);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath);
|
if (!buffer) throw new Error('Cannot resize image because mime type "' + mime + '" is not supported: ' + targetPath);
|
||||||
|
|
||||||
await shim.fsDriver().writeFile(targetPath, buffer, 'buffer');
|
await shim.fsDriver().writeFile(targetPath, buffer, 'buffer');
|
||||||
|
} else {
|
||||||
|
throw new Error('Node support not implemented');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resizeImage_ = async function(filePath, targetPath, mime) {
|
const resizeImage_ = async function(filePath, targetPath, mime) {
|
||||||
@ -146,7 +150,7 @@ function shimInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shim.attachFileToNote = async function(note, filePath, position = null) {
|
shim.attachFileToNote = async function(note, filePath, position = null) {
|
||||||
const resource = shim.createResourceFromPath(filePath);
|
const resource = await shim.createResourceFromPath(filePath);
|
||||||
|
|
||||||
const newBody = [];
|
const newBody = [];
|
||||||
|
|
||||||
@ -164,6 +168,20 @@ function shimInit() {
|
|||||||
return await Note.save(newNote);
|
return await Note.save(newNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shim.imageFromDataUrl = async function(imageDataUrl, filePath, options = null) {
|
||||||
|
if (options === null) options = {};
|
||||||
|
|
||||||
|
if (shim.isElectron()) {
|
||||||
|
const nativeImage = require('electron').nativeImage;
|
||||||
|
let image = nativeImage.createFromDataURL(imageDataUrl);
|
||||||
|
if (options.cropRect) image = image.crop(options.cropRect);
|
||||||
|
const mime = mimeUtils.fromDataUrl(imageDataUrl);
|
||||||
|
await shim.writeImageToFile(image, mime, filePath);
|
||||||
|
} else {
|
||||||
|
throw new Error('Node support not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nodeFetch = require('node-fetch');
|
const nodeFetch = require('node-fetch');
|
||||||
|
|
||||||
shim.readLocalFileBase64 = (path) => {
|
shim.readLocalFileBase64 = (path) => {
|
||||||
|
@ -136,6 +136,7 @@ shim.clearInterval = function(id) {
|
|||||||
shim.stringByteLength = function(string) { throw new Error('Not implemented'); }
|
shim.stringByteLength = function(string) { throw new Error('Not implemented'); }
|
||||||
shim.detectAndSetLocale = null;
|
shim.detectAndSetLocale = null;
|
||||||
shim.attachFileToNote = async (note, filePath) => {}
|
shim.attachFileToNote = async (note, filePath) => {}
|
||||||
|
shim.imageFromDataUrl = async function(imageDataUrl, filePath, options = null) { throw new Error('Not implemented') }
|
||||||
shim.Buffer = null;
|
shim.Buffer = null;
|
||||||
shim.openUrl = () => { throw new Error('Not implemented'); }
|
shim.openUrl = () => { throw new Error('Not implemented'); }
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user