1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Clipper: Allow selecting a folder and fixed screenshot taking issue

This commit is contained in:
Laurent Cozic 2018-05-26 15:46:57 +01:00
parent d6c6ef20d4
commit 89b486a3ee
10 changed files with 246 additions and 56 deletions

View File

@ -94,6 +94,7 @@
title: article.title, title: article.title,
baseUrl: baseUrl(), baseUrl: baseUrl(),
url: location.origin + location.pathname, url: location.origin + location.pathname,
parentId: command.parentId,
}; };
} else if (command.name === "completePageHtml") { } else if (command.name === "completePageHtml") {
@ -107,6 +108,7 @@
title: pageTitle(), title: pageTitle(),
baseUrl: baseUrl(), baseUrl: baseUrl(),
url: location.origin + location.pathname, url: location.origin + location.pathname,
parentId: command.parentId,
}; };
} else if (command.name === 'screenshot') { } else if (command.name === 'screenshot') {
@ -203,6 +205,7 @@
title: pageTitle(), title: pageTitle(),
cropRect: selectionArea, cropRect: selectionArea,
url: location.origin + location.pathname, url: location.origin + location.pathname,
parentId: command.parentId,
}; };
browser_.runtime.sendMessage({ browser_.runtime.sendMessage({

View File

@ -17,7 +17,8 @@
"tabs", "tabs",
"http://*/", "http://*/",
"https://*/", "https://*/",
"<all_urls>" "<all_urls>",
"storage"
], ],
"browser_action": { "browser_action": {

View File

@ -7,12 +7,13 @@
flex-direction: column; flex-direction: column;
background-color: #162b3d; background-color: #162b3d;
font-size: 16px; font-size: 16px;
color: #5A95C7;
padding: 10px;
box-sizing: border-box;
} }
.App h2 { .App h2 {
font-size: 1em; font-size: 1em;
color: #5A95C7;
padding-left: 10px;
margin-top: .5em; margin-top: .5em;
margin-bottom: .5em; margin-bottom: .5em;
font-weight: normal; font-weight: normal;
@ -28,13 +29,12 @@
.App .Controls { .App .Controls {
flex: 0; flex: 0;
padding-top: 10px;
} }
.App .Controls ul { .App .Controls ul {
flex: 0; flex: 0;
list-style-type: none; list-style-type: none;
padding: 0 10px; padding-left: 0;
margin: 0; margin: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -66,10 +66,8 @@
min-height: 0; min-height: 0;
flex: 1; flex: 1;
align-items: stretch; align-items: stretch;
margin: 0 10px 0 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/*border: 2px solid red;*/
} }
.App .Preview .Info { .App .Preview .Info {
@ -102,6 +100,23 @@
flex: 0; flex: 0;
} }
.App .Folders {
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 0;
}
.App .Folders label {
flex: 0;
white-space: nowrap;
}
.App .Folders select {
flex: 1;
margin-left: 10px;
}
.App .StatusBar { .App .StatusBar {
color: #5A95C7; color: #5A95C7;
font-size: .7em; font-size: .7em;
@ -109,8 +124,8 @@
flex: 0; flex: 0;
flex-direction: 'row'; flex-direction: 'row';
align-items: center; align-items: center;
min-height: 31px; min-height: 20px;
padding: 0px 10px 5px 10px; padding-top: 5px;
} }
.App .StatusBar .Led { .App .StatusBar .Led {

View File

@ -1,8 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import './App.css'; import './App.css';
import led_red from './led_red.png'; // Tell Webpack this JS file uses this image import led_red from './led_red.png';
import led_green from './led_green.png'; // Tell Webpack this JS file uses this image import led_green from './led_green.png';
import led_orange from './led_orange.png'; // Tell Webpack this JS file uses this image import led_orange from './led_orange.png';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { bridge } = require('./bridge'); const { bridge } = require('./bridge');
@ -27,13 +27,28 @@ class AppComponent extends Component {
}); });
} }
this.clipSimplified_click = () => {
bridge().sendCommandToActiveTab({
name: 'simplifiedPageHtml',
parentId: this.props.selectedFolderId,
});
}
this.clipComplete_click = () => {
bridge().sendCommandToActiveTab({
name: 'completePageHtml',
parentId: this.props.selectedFolderId,
});
}
this.clipScreenshot_click = async () => { this.clipScreenshot_click = async () => {
try { try {
const baseUrl = await bridge().clipperServerBaseUrl(); const baseUrl = await bridge().clipperServerBaseUrl();
bridge().sendCommandToActiveTab({ await bridge().sendCommandToActiveTab({
name: 'screenshot', name: 'screenshot',
apiBaseUrl: baseUrl, apiBaseUrl: baseUrl,
parentId: this.props.selectedFolderId,
}); });
window.close(); window.close();
@ -45,18 +60,13 @@ class AppComponent extends Component {
this.clipperServerHelpLink_click = () => { this.clipperServerHelpLink_click = () => {
bridge().tabsCreate({ url: 'https://joplin.cozic.net/clipper' }); bridge().tabsCreate({ url: 'https://joplin.cozic.net/clipper' });
} }
}
clipSimplified_click() { this.folderSelect_change = (event) => {
bridge().sendCommandToActiveTab({ this.props.dispatch({
name: 'simplifiedPageHtml', type: 'SELECTED_FOLDER_SET',
}); id: event.target.value,
} });
}
clipComplete_click() {
bridge().sendCommandToActiveTab({
name: 'completePageHtml',
});
} }
async loadContentScripts() { async loadContentScripts() {
@ -149,6 +159,36 @@ class AppComponent extends Component {
return <div className="StatusBar"><img className="Led" src={led}/><span className="ServerStatus">{ msg }{ helpLink }</span></div> return <div className="StatusBar"><img className="Led" src={led}/><span className="ServerStatus">{ msg }{ helpLink }</span></div>
} }
console.info(this.props.selectedFolderId);
const foldersComp = () => {
const optionComps = [];
const nonBreakingSpacify = (s) => {
// https://stackoverflow.com/a/24437562/561309
return s.replace(/ /g, "\u00a0");
}
const addOptions = (folders, depth) => {
for (let i = 0; i < folders.length; i++) {
const folder = folders[i];
optionComps.push(<option key={folder.id} value={folder.id}>{nonBreakingSpacify(' '.repeat(depth) + folder.title)}</option>)
if (folder.children) addOptions(folder.children, depth + 1);
}
}
addOptions(this.props.folders, 0);
return (
<div className="Folders">
<label>In notebook: </label>
<select defaultValue={this.props.selectedFolderId} onChange={this.folderSelect_change}>
{ optionComps }
</select>
</div>
);
}
return ( return (
<div className="App"> <div className="App">
<div className="Controls"> <div className="Controls">
@ -158,6 +198,7 @@ class AppComponent extends Component {
<li><a className="Button" onClick={this.clipScreenshot_click}>Clip screenshot</a></li> <li><a className="Button" onClick={this.clipScreenshot_click}>Clip screenshot</a></li>
</ul> </ul>
</div> </div>
{ foldersComp() }
{ warningComponent } { warningComponent }
<h2>Preview:</h2> <h2>Preview:</h2>
{ previewComponent } { previewComponent }
@ -174,6 +215,8 @@ const mapStateToProps = (state) => {
clippedContent: state.clippedContent, clippedContent: state.clippedContent,
contentUploadOperation: state.contentUploadOperation, contentUploadOperation: state.contentUploadOperation,
clipperServer: state.clipperServer, clipperServer: state.clipperServer,
folders: state.folders,
selectedFolderId: state.selectedFolderId,
}; };
}; };

View File

@ -27,6 +27,7 @@ class Bridge {
bodyHtml: command.html, bodyHtml: command.html,
baseUrl: command.baseUrl, baseUrl: command.baseUrl,
url: command.url, url: command.url,
parentId: command.parentId,
}; };
this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content }); this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content });
@ -52,6 +53,33 @@ class Bridge {
return this.dispatch_(action); return this.dispatch_(action);
} }
scheduleStateSave(state) {
if (this.scheduleStateSaveIID) {
clearTimeout(this.scheduleStateSaveIID);
this.scheduleStateSaveIID = null;
}
this.scheduleStateSaveIID = setTimeout(() => {
this.scheduleStateSaveIID = null;
const toSave = {
selectedFolderId: state.selectedFolderId,
};
console.info('Popup: Saving state', toSave);
this.storageSet(toSave);
}, 100);
}
async restoreState() {
const s = await this.storageGet(null);
console.info('Popup: Restoring saved state:', s);
if (!s) return;
if (s.selectedFolderId) this.dispatch({ type: 'SELECTED_FOLDER_SET', id: s.selectedFolderId });
}
async findClipperServerPort() { async findClipperServerPort() {
this.dispatch({ type: 'CLIPPER_SERVER_SET', foundState: 'searching' }); this.dispatch({ type: 'CLIPPER_SERVER_SET', foundState: 'searching' });
@ -68,6 +96,9 @@ class Bridge {
this.clipperServerPortStatus_ = 'found'; this.clipperServerPortStatus_ = 'found';
this.clipperServerPort_ = state.port; this.clipperServerPort_ = state.port;
this.dispatch({ type: 'CLIPPER_SERVER_SET', foundState: 'found', port: state.port }); this.dispatch({ type: 'CLIPPER_SERVER_SET', foundState: 'found', port: state.port });
const folders = await this.folderTree();
this.dispatch({ type: 'FOLDERS_SET', folders: folders });
return; return;
} }
} catch (error) { } catch (error) {
@ -152,6 +183,37 @@ class Bridge {
}); });
} }
async folderTree() {
return this.clipperApiExec('GET', 'folders');
}
async storageSet(keys) {
if (this.browserSupportsPromises_) return this.browser().storage.local.set(keys);
return new Promise((resolve, reject) => {
this.browser().storage.local.set(keys, () => {
resolve();
});
});
}
async storageGet(keys, defaultValue = null) {
if (this.browserSupportsPromises_) {
try {
const r = await this.browser().storage.local.get(keys);
return r;
} catch (error) {
return defaultValue;
}
} else {
return new Promise((resolve, reject) => {
this.browser().storage.local.get(keys, (result) => {
resolve(result);
});
});
}
}
async sendCommandToActiveTab(command) { async sendCommandToActiveTab(command) {
const tabs = await this.tabsQuery({ active: true, currentWindow: true }); const tabs = await this.tabsQuery({ active: true, currentWindow: true });
if (!tabs.length) { if (!tabs.length) {
@ -166,6 +228,30 @@ class Bridge {
await this.tabsSendMessage(tabs[0].id, command); await this.tabsSendMessage(tabs[0].id, command);
} }
async clipperApiExec(method, path, body) {
console.info('Popup: ' + method + ' ' + path);
const baseUrl = await this.clipperServerBaseUrl();
const fetchOptions = {
method: method,
headers: {
'Content-Type': 'application/json'
},
}
if (body) fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
const response = await fetch(baseUrl + "/" + path, fetchOptions)
if (!response.ok) {
const msg = await response.text();
throw new Error(msg);
}
const json = await response.json();
return json;
}
async sendContentToJoplin(content) { async sendContentToJoplin(content) {
console.info('Popup: Sending to Joplin...'); console.info('Popup: Sending to Joplin...');
@ -176,20 +262,9 @@ class Bridge {
const baseUrl = await this.clipperServerBaseUrl(); const baseUrl = await this.clipperServerBaseUrl();
const response = await fetch(baseUrl + "/notes", { await this.clipperApiExec('POST', 'notes', content);
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(content)
})
if (!response.ok) { this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: response.text() } });
} else {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
}
} catch (error) { } catch (error) {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: error.message } }); this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: error.message } });
} }

View File

@ -5,7 +5,7 @@ import App from './App';
const { Provider } = require('react-redux'); const { Provider } = require('react-redux');
const { bridge } = require('./bridge'); const { bridge } = require('./bridge');
const { createStore } = require('redux'); const { createStore, applyMiddleware } = require('redux');
const defaultState = { const defaultState = {
warning: '', warning: '',
@ -15,8 +15,21 @@ const defaultState = {
foundState: 'idle', foundState: 'idle',
port: null, port: null,
}, },
folders: [],
selectedFolderId: null,
}; };
const reduxMiddleware = store => next => async (action) => {
const result = next(action);
const newState = store.getState();
if (['SELECTED_FOLDER_SET'].indexOf(action.type) >= 0) {
bridge().scheduleStateSave(newState);
}
return result;
}
function reducer(state = defaultState, action) { function reducer(state = defaultState, action) {
let newState = state; let newState = state;
@ -42,6 +55,16 @@ function reducer(state = defaultState, action) {
newState = Object.assign({}, state); newState = Object.assign({}, state);
newState.contentUploadOperation = action.operation; newState.contentUploadOperation = action.operation;
} else if (action.type === 'FOLDERS_SET') {
newState = Object.assign({}, state);
newState.folders = action.folders;
} else if (action.type === 'SELECTED_FOLDER_SET') {
newState = Object.assign({}, state);
newState.selectedFolderId = action.id;
} else if (action.type === 'CLIPPER_SERVER_SET') { } else if (action.type === 'CLIPPER_SERVER_SET') {
newState = Object.assign({}, state); newState = Object.assign({}, state);
@ -55,9 +78,10 @@ function reducer(state = defaultState, action) {
return newState; return newState;
} }
const store = createStore(reducer); const store = createStore(reducer, applyMiddleware(reduxMiddleware));
bridge().init(window.browser ? window.browser : window.chrome, !!window.browser, store.dispatch); bridge().init(window.browser ? window.browser : window.chrome, !!window.browser, store.dispatch);
bridge().restoreState();
console.info('Popup: Creating React app...'); console.info('Popup: Creating React app...');

View File

@ -161,7 +161,10 @@ class BaseModel {
} }
static async all(options = null) { static async all(options = null) {
let q = this.applySqlOptions(options, 'SELECT * FROM `' + this.tableName() + '`'); if (!options) options = {};
if (!options.fields) options.fields = '*';
let q = this.applySqlOptions(options, 'SELECT ' + this.db().escapeFields(options.fields) + ' FROM `' + this.tableName() + '`');
return this.modelSelectAll(q.sql); return this.modelSelectAll(q.sql);
} }

View File

@ -64,14 +64,6 @@ class ClipperServer {
}); });
} }
// startState() {
// return this.startState_;
// }
// port() {
// return this.port_;
// }
htmlToMdParser() { htmlToMdParser() {
if (this.htmlToMdParser_) return this.htmlToMdParser_; if (this.htmlToMdParser_) return this.htmlToMdParser_;
this.htmlToMdParser_ = new HtmlToMd(); this.htmlToMdParser_ = new HtmlToMd();
@ -93,8 +85,8 @@ class ClipperServer {
}); });
} }
if (requestNote.parent_id) { if (requestNote.parentId) {
output.parent_id = requestNote.parent_id; output.parent_id = requestNote.parentId;
} else { } else {
const folder = await Folder.defaultFolder(); const folder = await Folder.defaultFolder();
if (!folder) throw new Error('Cannot find folder for note'); if (!folder) throw new Error('Cannot find folder for note');
@ -227,11 +219,11 @@ class ClipperServer {
this.server_ = require('http').createServer(); this.server_ = require('http').createServer();
this.server_.on('request', (request, response) => { this.server_.on('request', async (request, response) => {
const writeCorsHeaders = (code) => { const writeCorsHeaders = (code, contentType = "application/json") => {
response.writeHead(code, { response.writeHead(code, {
"Content-Type": "application/json", "Content-Type": contentType,
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'X-Requested-With,content-type', 'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
@ -245,7 +237,7 @@ class ClipperServer {
} }
const writeResponseText = (code, text) => { const writeResponseText = (code, text) => {
writeCorsHeaders(code); writeCorsHeaders(code, 'text/plain');
response.write(text); response.write(text);
response.end(); response.end();
} }
@ -259,6 +251,11 @@ class ClipperServer {
if (url.pathname === '/ping') { if (url.pathname === '/ping') {
return writeResponseText(200, 'JoplinClipperServer'); return writeResponseText(200, 'JoplinClipperServer');
} }
if (url.pathname === '/folders') {
const structure = await Folder.allAsTree({ fields: ['id', 'parent_id', 'title'] });
return writeResponseJson(200, structure);
}
} else if (request.method === 'POST') { } else if (request.method === 'POST') {
if (url.pathname === '/notes') { if (url.pathname === '/notes') {
let body = ''; let body = '';

View File

@ -127,6 +127,34 @@ class Folder extends BaseItem {
return output; return output;
} }
static async allAsTree(options = null) {
const all = await this.all(options);
// https://stackoverflow.com/a/49387427/561309
function getNestedChildren(models, parentId) {
const nestedTreeStructure = [];
const length = models.length;
for (let i = 0; i < length; i++) {
const model = models[i];
if (model.parent_id == parentId) {
const children = getNestedChildren(models, model.id);
if (children.length > 0) {
model.children = children;
}
nestedTreeStructure.push(model);
}
}
return nestedTreeStructure;
}
return getNestedChildren(all, '');
}
static load(id) { static load(id) {
if (id == this.conflictFolderId()) return this.conflictFolder(); if (id == this.conflictFolderId()) return this.conflictFolder();
return super.load(id); return super.load(id);

View File

@ -61,6 +61,7 @@
"_releases", "_releases",
"ReactNativeClient/lib/csstojs", "ReactNativeClient/lib/csstojs",
"Clipper/joplin-webclipper/popup/build", "Clipper/joplin-webclipper/popup/build",
"Clipper/joplin-webclipper/dist",
], ],
"path": "." "path": "."
}, },