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

Added support for enex import

This commit is contained in:
Laurent Cozic 2017-11-11 17:36:47 +00:00
parent 6b3bda2941
commit e649670bfe
11 changed files with 376 additions and 90 deletions

View File

@ -24,13 +24,20 @@ const appDefaultState = Object.assign({}, defaultState, {
route: {
type: 'NAV_GO',
routeName: 'Main',
params: {},
props: {},
},
navHistory: [],
fileToImport: null,
windowCommand: null,
});
class Application extends BaseApplication {
constructor() {
super();
this.lastMenuScreen_ = null;
}
hasGui() {
return true;
}
@ -76,6 +83,12 @@ class Application extends BaseApplication {
newState.windowContentSize = action.size;
break;
case 'WINDOW_COMMAND':
newState = Object.assign({}, state);
newState.windowCommand = { name: action.name };
break;
}
} catch (error) {
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
@ -90,16 +103,75 @@ class Application extends BaseApplication {
if (!await reg.syncStarted()) reg.scheduleSync();
}
return super.generalMiddleware(store, next, action);
const result = await super.generalMiddleware(store, next, action);
const newState = store.getState();
if (action.type === 'NAV_GO' || action.type === 'NAV_BACK') {
app().updateMenu(newState.route.routeName);
}
return result;
}
setupMenu() {
updateMenu(screen) {
if (this.lastMenuScreen_ === screen) return;
const template = [
{
label: 'File',
submenu: [{
label: _('New note'),
accelerator: 'CommandOrControl+N',
screens: ['Main'],
click: () => {
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'newNote',
});
}
}, {
label: _('New to-do'),
accelerator: 'CommandOrControl+T',
screens: ['Main'],
click: () => {
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'newTodo',
});
}
}, {
label: _('New notebook'),
screens: ['Main'],
click: () => {
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'newNotebook',
});
}
}, {
type: 'separator',
}, {
label: _('Import Evernote notes'),
click () { }
click: () => {
const filePaths = bridge().showOpenDialog({
properties: ['openFile', 'createDirectory'],
filters: [
{ name: _('Evernote Export Files'), extensions: ['enex'] },
]
});
if (!filePaths || !filePaths.length) return;
this.dispatch({
type: 'NAV_GO',
routeName: 'Import',
props: {
filePath: filePaths[0],
},
});
}
}, {
type: 'separator',
}, {
label: _('Quit'),
accelerator: 'CommandOrControl+Q',
@ -113,19 +185,34 @@ class Application extends BaseApplication {
click () { bridge().openExternal('http://joplin.cozic.net') }
}, {
label: _('About Joplin'),
click () { }
click () { }
}]
},
]
];
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
function removeUnwantedItems(template, screen) {
let output = [];
for (let i = 0; i < template.length; i++) {
const t = Object.assign({}, template[i]);
if (t.screens && t.screens.indexOf(screen) < 0) continue;
if (t.submenu) t.submenu = removeUnwantedItems(t.submenu, screen);
output.push(t);
}
return output;
}
let screenTemplate = removeUnwantedItems(template, screen);
const menu = Menu.buildFromTemplate(screenTemplate);
Menu.setApplicationMenu(menu);
this.lastMenuScreen_ = screen;
}
async start(argv) {
argv = await super.start(argv);
this.setupMenu();
this.updateMenu('Main');
this.initRedux();

View File

@ -35,9 +35,6 @@ class HeaderComponent extends React.Component {
style.boxSizing = 'border-box';
const buttons = [];
if (showBackButton) {
buttons.push(this.makeButton('back', {}, { title: _('Back'), onClick: () => this.back_click() }));
}
const buttonStyle = {
height: theme.headerHeight,
@ -53,6 +50,10 @@ class HeaderComponent extends React.Component {
cursor: 'default',
};
if (showBackButton) {
buttons.push(this.makeButton('back', buttonStyle, { title: _('Back'), onClick: () => this.back_click(), iconName: 'fa-chevron-left ' }));
}
if (this.props.buttons) {
for (let i = 0; i < this.props.buttons.length; i++) {
const o = this.props.buttons[i];

View File

@ -0,0 +1,136 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const { Folder } = require('lib/models/folder.js');
const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const { filename, basename } = require('lib/path-utils.js');
const { importEnex } = require('lib/import-enex');
class ImportScreenComponent extends React.Component {
componentWillMount() {
this.setState({
doImport: true,
filePath: this.props.filePath,
messages: [],
});
}
componentWillReceiveProps(newProps) {
if (newProps.filePath) {
this.setState({
doImport: true,
filePath: newProps.filePath,
messages: [],
});
this.doImport();
}
}
componentDidMount() {
if (this.state.filePath && this.state.doImport) {
this.doImport();
}
}
addMessage(key, text) {
const messages = this.state.messages.slice();
let found = false;
for (let i = 0; i < messages.length; i++) {
if (messages[i].key === key) {
messages[i].text = text;
found = true;
break;
}
}
if (!found) messages.push({ key: key, text: text });
this.setState({ messages: messages });
}
async doImport() {
const filePath = this.props.filePath;
const folderTitle = await Folder.findUniqueFolderTitle(filename(filePath));
const messages = this.state.messages.slice();
this.addMessage('start', _('New notebook "%s" will be created and file "%s" will be imported into it', folderTitle, basename(filePath)));
let lastProgress = '';
let progressCount = 0;
const options = {
onProgress: (progressState) => {
let line = [];
line.push(_('Found: %d.', progressState.loaded));
line.push(_('Created: %d.', progressState.created));
if (progressState.updated) line.push(_('Updated: %d.', progressState.updated));
if (progressState.skipped) line.push(_('Skipped: %d.', progressState.skipped));
if (progressState.resourcesCreated) line.push(_('Resources: %d.', progressState.resourcesCreated));
if (progressState.notesTagged) line.push(_('Tagged: %d.', progressState.notesTagged));
lastProgress = line.join(' ');
this.addMessage('progress', lastProgress);
},
onError: (error) => {
const messages = this.state.messages.slice();
let s = error.trace ? error.trace : error.toString();
messages.push({ key: 'error_' + (progressCount++), text: s });
this.addMessage('error_' + (progressCount++), lastProgress);
},
}
// const folder = await Folder.save({ title: folderTitle });
// await importEnex(folder.id, filePath, options);
this.addMessage('done', _('The notes have been imported: %s', lastProgress));
this.setState({ doImport: false });
}
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
const messages = this.state.messages;
const messagesStyle = {
padding: 10,
fontSize: theme.fontSize,
fontFamily: theme.fontFamily,
backgroundColor: theme.backgroundColor,
};
const headerStyle = {
width: style.width,
};
const messageComps = [];
for (let i = 0; i < messages.length; i++) {
messageComps.push(<div key={messages[i].key}>{messages[i].text}</div>);
}
return (
<div style={{}}>
<Header style={headerStyle} />
<div style={messagesStyle}>
{messageComps}
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
theme: state.settings.theme,
};
};
const ImportScreen = connect(mapStateToProps)(ImportScreenComponent);
module.exports = { ImportScreen };

View File

@ -24,6 +24,12 @@ class MainScreenComponent extends React.Component {
});
}
componentWillReceiveProps(newProps) {
if (newProps.windowCommand) {
this.doCommand(newProps.windowCommand);
}
}
toggleVisiblePanes() {
let panes = this.state.noteVisiblePanes.slice();
if (panes.length === 2) {
@ -37,6 +43,84 @@ class MainScreenComponent extends React.Component {
this.setState({ noteVisiblePanes: panes });
}
doCommand(command) {
if (!command) return;
const createNewNote = async (title, isTodo) => {
const folderId = Setting.value('activeFolderId');
if (!folderId) return;
const note = await Note.save({
title: title,
parent_id: folderId,
is_todo: isTodo ? 1 : 0,
});
Note.updateGeolocation(note.id);
this.props.dispatch({
type: 'NOTE_SELECT',
id: note.id,
});
}
let commandProcessed = true;
if (command.name === 'newNote') {
this.setState({
promptOptions: {
message: _('Note title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, false);
this.setState({ promptOptions: null });
}
},
});
} else if (command.name === 'newTodo') {
this.setState({
promptOptions: {
message: _('To-do title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, true);
this.setState({ promptOptions: null });
}
},
});
} else if (command.name === 'newNotebook') {
this.setState({
promptOptions: {
message: _('Notebook title:'),
onClose: async (answer) => {
if (answer) {
let folder = null;
try {
folder = await Folder.save({ title: answer }, { userSideValidation: true });
} catch (error) {
bridge().showErrorMessageBox(error.message);
return;
}
this.props.dispatch({
type: 'FOLDER_SELECT',
id: folder.id,
});
}
this.setState({ promptOptions: null });
}
},
});
} else {
commandProcessed = false;
}
if (commandProcessed) {
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: null,
});
}
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
@ -74,85 +158,24 @@ class MainScreenComponent extends React.Component {
height: style.height,
};
const createNewNote = async (title, isTodo) => {
const folderId = Setting.value('activeFolderId');
if (!folderId) return;
const note = await Note.save({
title: title,
parent_id: folderId,
is_todo: isTodo ? 1 : 0,
});
Note.updateGeolocation(note.id);
this.props.dispatch({
type: 'NOTE_SELECT',
id: note.id,
});
}
const headerButtons = [];
headerButtons.push({
title: _('New note'),
iconName: 'fa-file-o',
onClick: () => {
this.setState({
promptOptions: {
message: _('Note title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, false);
this.setState({ promptOptions: null });
}
},
});
},
onClick: () => { this.doCommand({ name: 'newNote' }) },
});
headerButtons.push({
title: _('New to-do'),
iconName: 'fa-check-square-o',
onClick: () => {
this.setState({
promptOptions: {
message: _('Note title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, true);
this.setState({ promptOptions: null });
}
},
});
},
onClick: () => { this.doCommand({ name: 'newTodo' }) },
});
headerButtons.push({
title: _('New notebook'),
iconName: 'fa-folder-o',
onClick: () => {
this.setState({
promptOptions: {
message: _('Notebook title:'),
onClose: async (answer) => {
if (answer) {
let folder = null;
try {
folder = await Folder.save({ title: answer }, { userSideValidation: true });
} catch (error) {
bridge().showErrorMessageBox(error.message);
return;
}
this.props.dispatch({
type: 'FOLDER_SELECT',
id: folder.id,
});
}
this.setState({ promptOptions: null });
}
},
});
},
onClick: () => { this.doCommand({ name: 'newNotebook' }) },
});
headerButtons.push({
@ -179,6 +202,7 @@ class MainScreenComponent extends React.Component {
const mapStateToProps = (state) => {
return {
theme: state.settings.theme,
windowCommand: state.windowCommand,
};
};

View File

@ -1,5 +1,6 @@
const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux');
const { app } = require('../app.js');
class NavigatorComponent extends Component {
@ -7,6 +8,7 @@ class NavigatorComponent extends Component {
if (!this.props.route) throw new Error('Route must not be null');
const route = this.props.route;
const screenProps = route.props ? route.props : {};
const Screen = this.props.screens[route.routeName].screen;
const screenStyle = {
@ -16,7 +18,7 @@ class NavigatorComponent extends Component {
return (
<div style={this.props.style}>
<Screen style={screenStyle}/>
<Screen style={screenStyle} {...screenProps}/>
</div>
);
}

View File

@ -5,6 +5,7 @@ const { connect, Provider } = require('react-redux');
const { MainScreen } = require('./MainScreen.min.js');
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
const { ImportScreen } = require('./ImportScreen.min.js');
const { Navigator } = require('./Navigator.min.js');
const { app } = require('../app');
@ -52,6 +53,7 @@ class RootComponent extends React.Component {
const screens = {
Main: { screen: MainScreen },
OneDriveLogin: { screen: OneDriveLoginScreen },
Import: { screen: ImportScreen },
};
return (

View File

@ -1114,8 +1114,7 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"create-error-class": {
"version": "3.0.2",
@ -2261,8 +2260,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.4",
@ -2490,8 +2488,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isbinaryfile": {
"version": "3.0.2",
@ -2611,6 +2608,11 @@
"verror": "1.10.0"
}
},
"jssha": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@ -2653,6 +2655,11 @@
"invert-kv": "1.0.0"
}
},
"levenshtein": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/levenshtein/-/levenshtein-1.0.5.tgz",
"integrity": "sha1-ORFzepy1baNF0Aj1V4LG8TiXm6M="
},
"linkify-it": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz",
@ -3486,8 +3493,7 @@
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
"dev": true
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
},
"progress-stream": {
"version": "1.2.0",
@ -3726,7 +3732,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
@ -4899,6 +4904,20 @@
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
"string-padding": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-padding/-/string-padding-1.0.2.tgz",
"integrity": "sha1-OqrYVbPpc1xeQS3+chmMz5nH9I4="
},
"string-to-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.0.tgz",
"integrity": "sha1-rPLJ6tHEGOFIUJoS0su0afMzohg=",
"requires": {
"inherits": "2.0.3",
"readable-stream": "2.3.3"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@ -4914,7 +4933,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
@ -5270,8 +5288,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.1.0",

View File

@ -31,6 +31,8 @@
"fs-extra": "^4.0.2",
"highlight.js": "^9.12.0",
"html-entities": "^1.2.1",
"jssha": "^2.3.1",
"levenshtein": "^1.0.5",
"lodash": "^4.17.4",
"markdown-it": "^8.4.0",
"marked": "^0.3.6",
@ -48,6 +50,8 @@
"sharp": "^0.18.4",
"sprintf-js": "^1.1.1",
"sqlite3": "^3.1.13",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"tcp-port-used": "^0.1.2"
}
}

View File

@ -12,7 +12,7 @@ const { time } = require('lib/time-utils.js');
const Levenshtein = require('levenshtein');
const jsSHA = require("jssha");
const Promise = require('promise');
//const Promise = require('promise');
const fs = require('fs-extra');
const stringToStream = require('string-to-stream')

View File

@ -34,6 +34,19 @@ class Folder extends BaseItem {
}
}
static async findUniqueFolderTitle(title) {
let counter = 1;
let titleToTry = title;
while (true) {
const folder = await this.loadByField('title', titleToTry);
if (!folder) return titleToTry;
titleToTry = title + ' (' + counter + ')';
counter++;
if (counter >= 100) titleToTry = title + ' (' + ((new Date()).getTime()) + ')';
if (counter >= 1000) throw new Error('Cannot find unique title');
}
}
static noteIds(parentId) {
return this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]).then((rows) => {
let output = [];