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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import React, { Component } from 'react';
import './App.css';
import led_red from './led_red.png'; // Tell Webpack this JS file uses this image
import led_green from './led_green.png'; // Tell Webpack this JS file uses this image
import led_orange from './led_orange.png'; // Tell Webpack this JS file uses this image
import led_red from './led_red.png';
import led_green from './led_green.png';
import led_orange from './led_orange.png';
const { connect } = require('react-redux');
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 () => {
try {
const baseUrl = await bridge().clipperServerBaseUrl();
bridge().sendCommandToActiveTab({
await bridge().sendCommandToActiveTab({
name: 'screenshot',
apiBaseUrl: baseUrl,
parentId: this.props.selectedFolderId,
});
window.close();
@ -45,18 +60,13 @@ class AppComponent extends Component {
this.clipperServerHelpLink_click = () => {
bridge().tabsCreate({ url: 'https://joplin.cozic.net/clipper' });
}
}
clipSimplified_click() {
bridge().sendCommandToActiveTab({
name: 'simplifiedPageHtml',
this.folderSelect_change = (event) => {
this.props.dispatch({
type: 'SELECTED_FOLDER_SET',
id: event.target.value,
});
}
clipComplete_click() {
bridge().sendCommandToActiveTab({
name: 'completePageHtml',
});
}
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>
}
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 (
<div className="App">
<div className="Controls">
@ -158,6 +198,7 @@ class AppComponent extends Component {
<li><a className="Button" onClick={this.clipScreenshot_click}>Clip screenshot</a></li>
</ul>
</div>
{ foldersComp() }
{ warningComponent }
<h2>Preview:</h2>
{ previewComponent }
@ -174,6 +215,8 @@ const mapStateToProps = (state) => {
clippedContent: state.clippedContent,
contentUploadOperation: state.contentUploadOperation,
clipperServer: state.clipperServer,
folders: state.folders,
selectedFolderId: state.selectedFolderId,
};
};

View File

@ -27,6 +27,7 @@ class Bridge {
bodyHtml: command.html,
baseUrl: command.baseUrl,
url: command.url,
parentId: command.parentId,
};
this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content });
@ -52,6 +53,33 @@ class Bridge {
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() {
this.dispatch({ type: 'CLIPPER_SERVER_SET', foundState: 'searching' });
@ -68,6 +96,9 @@ class Bridge {
this.clipperServerPortStatus_ = 'found';
this.clipperServerPort_ = 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;
}
} 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) {
const tabs = await this.tabsQuery({ active: true, currentWindow: true });
if (!tabs.length) {
@ -166,6 +228,30 @@ class Bridge {
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) {
console.info('Popup: Sending to Joplin...');
@ -176,20 +262,9 @@ class Bridge {
const baseUrl = await this.clipperServerBaseUrl();
const response = await fetch(baseUrl + "/notes", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(content)
})
await this.clipperApiExec('POST', 'notes', content);
if (!response.ok) {
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) {
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 { bridge } = require('./bridge');
const { createStore } = require('redux');
const { createStore, applyMiddleware } = require('redux');
const defaultState = {
warning: '',
@ -15,8 +15,21 @@ const defaultState = {
foundState: 'idle',
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) {
let newState = state;
@ -42,6 +55,16 @@ function reducer(state = defaultState, action) {
newState = Object.assign({}, state);
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') {
newState = Object.assign({}, state);
@ -55,9 +78,10 @@ function reducer(state = defaultState, action) {
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().restoreState();
console.info('Popup: Creating React app...');

View File

@ -161,7 +161,10 @@ class BaseModel {
}
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);
}

View File

@ -64,14 +64,6 @@ class ClipperServer {
});
}
// startState() {
// return this.startState_;
// }
// port() {
// return this.port_;
// }
htmlToMdParser() {
if (this.htmlToMdParser_) return this.htmlToMdParser_;
this.htmlToMdParser_ = new HtmlToMd();
@ -93,8 +85,8 @@ class ClipperServer {
});
}
if (requestNote.parent_id) {
output.parent_id = requestNote.parent_id;
if (requestNote.parentId) {
output.parent_id = requestNote.parentId;
} else {
const folder = await Folder.defaultFolder();
if (!folder) throw new Error('Cannot find folder for note');
@ -227,11 +219,11 @@ class ClipperServer {
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, {
"Content-Type": "application/json",
"Content-Type": contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
@ -245,7 +237,7 @@ class ClipperServer {
}
const writeResponseText = (code, text) => {
writeCorsHeaders(code);
writeCorsHeaders(code, 'text/plain');
response.write(text);
response.end();
}
@ -259,6 +251,11 @@ class ClipperServer {
if (url.pathname === '/ping') {
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') {
if (url.pathname === '/notes') {
let body = '';

View File

@ -127,6 +127,34 @@ class Folder extends BaseItem {
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) {
if (id == this.conflictFolderId()) return this.conflictFolder();
return super.load(id);

View File

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