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:
parent
d6c6ef20d4
commit
89b486a3ee
@ -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({
|
||||
|
@ -17,7 +17,8 @@
|
||||
"tabs",
|
||||
"http://*/",
|
||||
"https://*/",
|
||||
"<all_urls>"
|
||||
"<all_urls>",
|
||||
"storage"
|
||||
],
|
||||
|
||||
"browser_action": {
|
||||
|
@ -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 {
|
||||
|
@ -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');
|
||||
@ -24,16 +24,31 @@ class AppComponent extends Component {
|
||||
this.props.dispatch({
|
||||
type: 'CLIPPED_CONTENT_TITLE_SET',
|
||||
text: event.currentTarget.value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
clipComplete_click() {
|
||||
bridge().sendCommandToActiveTab({
|
||||
name: 'completePageHtml',
|
||||
});
|
||||
this.folderSelect_change = (event) => {
|
||||
this.props.dispatch({
|
||||
type: 'SELECTED_FOLDER_SET',
|
||||
id: event.target.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadContentScripts() {
|
||||
@ -147,7 +157,37 @@ class AppComponent extends Component {
|
||||
msg = "Service status: " + msg
|
||||
|
||||
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">
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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 } });
|
||||
}
|
||||
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 } });
|
||||
}
|
||||
|
@ -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...');
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 = '';
|
||||
|
@ -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);
|
||||
|
@ -61,6 +61,7 @@
|
||||
"_releases",
|
||||
"ReactNativeClient/lib/csstojs",
|
||||
"Clipper/joplin-webclipper/popup/build",
|
||||
"Clipper/joplin-webclipper/dist",
|
||||
],
|
||||
"path": "."
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user