diff --git a/ElectronClient/app/app.js b/ElectronClient/app/app.js
index e297e7d41..92f71c88e 100644
--- a/ElectronClient/app/app.js
+++ b/ElectronClient/app/app.js
@@ -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();
diff --git a/ElectronClient/app/gui/Header.jsx b/ElectronClient/app/gui/Header.jsx
index 3568cdfc0..3eaa59f4e 100644
--- a/ElectronClient/app/gui/Header.jsx
+++ b/ElectronClient/app/gui/Header.jsx
@@ -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];
diff --git a/ElectronClient/app/gui/ImportScreen.jsx b/ElectronClient/app/gui/ImportScreen.jsx
new file mode 100644
index 000000000..d9f296dff
--- /dev/null
+++ b/ElectronClient/app/gui/ImportScreen.jsx
@@ -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(
{messages[i].text}
);
+ }
+
+ return (
+
+ );
+ }
+
+}
+
+const mapStateToProps = (state) => {
+ return {
+ theme: state.settings.theme,
+ };
+};
+
+const ImportScreen = connect(mapStateToProps)(ImportScreenComponent);
+
+module.exports = { ImportScreen };
\ No newline at end of file
diff --git a/ElectronClient/app/gui/MainScreen.jsx b/ElectronClient/app/gui/MainScreen.jsx
index e7654d6d4..f666cd4df 100644
--- a/ElectronClient/app/gui/MainScreen.jsx
+++ b/ElectronClient/app/gui/MainScreen.jsx
@@ -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,
};
};
diff --git a/ElectronClient/app/gui/Navigator.jsx b/ElectronClient/app/gui/Navigator.jsx
index 1fb1adc4c..db0220aad 100644
--- a/ElectronClient/app/gui/Navigator.jsx
+++ b/ElectronClient/app/gui/Navigator.jsx
@@ -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 (
-
+
);
}
diff --git a/ElectronClient/app/gui/Root.jsx b/ElectronClient/app/gui/Root.jsx
index 0326e8bfb..7661deff0 100644
--- a/ElectronClient/app/gui/Root.jsx
+++ b/ElectronClient/app/gui/Root.jsx
@@ -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 (
diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json
index 1ffb48a48..908ef8018 100644
--- a/ElectronClient/app/package-lock.json
+++ b/ElectronClient/app/package-lock.json
@@ -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",
diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json
index 9386088ed..a1a8d2a39 100644
--- a/ElectronClient/app/package.json
+++ b/ElectronClient/app/package.json
@@ -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"
}
}
diff --git a/CliClient/app/import-enex-md-gen.js b/ReactNativeClient/lib/import-enex-md-gen.js
similarity index 100%
rename from CliClient/app/import-enex-md-gen.js
rename to ReactNativeClient/lib/import-enex-md-gen.js
diff --git a/CliClient/app/import-enex.js b/ReactNativeClient/lib/import-enex.js
similarity index 99%
rename from CliClient/app/import-enex.js
rename to ReactNativeClient/lib/import-enex.js
index 93d424a58..f10de89dc 100644
--- a/CliClient/app/import-enex.js
+++ b/ReactNativeClient/lib/import-enex.js
@@ -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')
diff --git a/ReactNativeClient/lib/models/folder.js b/ReactNativeClient/lib/models/folder.js
index 45b9ddf11..a7d68d915 100644
--- a/ReactNativeClient/lib/models/folder.js
+++ b/ReactNativeClient/lib/models/folder.js
@@ -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 = [];