2017-11-04 15:31:51 +02:00
|
|
|
require('app-module-path').addPath(__dirname);
|
|
|
|
|
2017-11-04 14:23:46 +02:00
|
|
|
const { BaseApplication } = require('lib/BaseApplication');
|
2017-11-04 15:23:15 +02:00
|
|
|
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
2017-11-04 14:23:46 +02:00
|
|
|
const { Setting } = require('lib/models/setting.js');
|
2017-11-14 20:02:58 +02:00
|
|
|
const { shim } = require('lib/shim.js');
|
2017-11-04 14:23:46 +02:00
|
|
|
const { BaseModel } = require('lib/base-model.js');
|
2017-11-12 02:44:26 +02:00
|
|
|
const { _, setLocale } = require('lib/locale.js');
|
2017-11-04 14:23:46 +02:00
|
|
|
const os = require('os');
|
|
|
|
const fs = require('fs-extra');
|
2017-11-04 15:23:15 +02:00
|
|
|
const { Tag } = require('lib/models/tag.js');
|
2017-11-04 14:23:46 +02:00
|
|
|
const { reg } = require('lib/registry.js');
|
|
|
|
const { sprintf } = require('sprintf-js');
|
|
|
|
const { JoplinDatabase } = require('lib/joplin-database.js');
|
|
|
|
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
2017-11-04 14:38:53 +02:00
|
|
|
const { ElectronAppWrapper } = require('./ElectronAppWrapper');
|
2017-11-06 20:35:04 +02:00
|
|
|
const { defaultState } = require('lib/reducer.js');
|
2017-11-28 21:53:29 +02:00
|
|
|
const packageInfo = require('./packageInfo.js');
|
2017-11-28 00:50:46 +02:00
|
|
|
const AlarmService = require('lib/services/AlarmService.js');
|
|
|
|
const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode');
|
2017-11-04 14:23:46 +02:00
|
|
|
|
2017-11-11 14:00:37 +02:00
|
|
|
const { bridge } = require('electron').remote.require('./bridge');
|
|
|
|
const Menu = bridge().Menu;
|
|
|
|
const MenuItem = bridge().MenuItem;
|
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
const appDefaultState = Object.assign({}, defaultState, {
|
|
|
|
route: {
|
|
|
|
type: 'NAV_GO',
|
|
|
|
routeName: 'Main',
|
2017-11-11 19:36:47 +02:00
|
|
|
props: {},
|
2017-11-06 20:35:04 +02:00
|
|
|
},
|
|
|
|
navHistory: [],
|
2017-11-11 19:36:47 +02:00
|
|
|
fileToImport: null,
|
|
|
|
windowCommand: null,
|
2017-11-12 19:02:20 +02:00
|
|
|
noteVisiblePanes: ['editor', 'viewer'],
|
2017-11-14 20:02:58 +02:00
|
|
|
windowContentSize: bridge().windowContentSize(),
|
2017-11-14 01:04:27 +02:00
|
|
|
});
|
2017-11-04 14:23:46 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
class Application extends BaseApplication {
|
2017-11-04 14:23:46 +02:00
|
|
|
|
2017-11-11 19:36:47 +02:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.lastMenuScreen_ = null;
|
|
|
|
}
|
|
|
|
|
2017-11-05 02:17:48 +02:00
|
|
|
hasGui() {
|
|
|
|
return true;
|
2017-11-04 14:23:46 +02:00
|
|
|
}
|
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
reducer(state = appDefaultState, action) {
|
|
|
|
let newState = state;
|
2017-11-04 14:23:46 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
try {
|
|
|
|
switch (action.type) {
|
2017-11-04 15:23:15 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
case 'NAV_BACK':
|
|
|
|
case 'NAV_GO':
|
2017-11-04 15:23:15 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
const goingBack = action.type === 'NAV_BACK';
|
|
|
|
|
|
|
|
if (goingBack && !state.navHistory.length) break;
|
|
|
|
|
|
|
|
const currentRoute = state.route;
|
|
|
|
|
|
|
|
newState = Object.assign({}, state);
|
|
|
|
let newNavHistory = state.navHistory.slice();
|
|
|
|
|
|
|
|
if (goingBack) {
|
|
|
|
let newAction = null;
|
|
|
|
while (newNavHistory.length) {
|
|
|
|
newAction = newNavHistory.pop();
|
|
|
|
if (newAction.routeName !== state.route.routeName) break;
|
|
|
|
}
|
2017-11-04 15:23:15 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
if (!newAction) break;
|
2017-11-04 15:23:15 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
action = newAction;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!goingBack) newNavHistory.push(currentRoute);
|
|
|
|
newState.navHistory = newNavHistory
|
|
|
|
newState.route = action;
|
|
|
|
break;
|
2017-11-04 15:23:15 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
case 'WINDOW_CONTENT_SIZE_SET':
|
2017-11-04 15:23:15 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
newState = Object.assign({}, state);
|
|
|
|
newState.windowContentSize = action.size;
|
|
|
|
break;
|
2017-11-04 15:23:15 +02:00
|
|
|
|
2017-11-11 19:36:47 +02:00
|
|
|
case 'WINDOW_COMMAND':
|
|
|
|
|
|
|
|
newState = Object.assign({}, state);
|
2017-11-12 01:13:14 +02:00
|
|
|
let command = Object.assign({}, action);
|
|
|
|
delete command.type;
|
|
|
|
newState.windowCommand = command;
|
2017-11-11 19:36:47 +02:00
|
|
|
break;
|
|
|
|
|
2017-11-12 19:02:20 +02:00
|
|
|
case 'NOTE_VISIBLE_PANES_TOGGLE':
|
|
|
|
|
|
|
|
let panes = state.noteVisiblePanes.slice();
|
|
|
|
if (panes.length === 2) {
|
|
|
|
panes = ['editor'];
|
|
|
|
} else if (panes.indexOf('editor') >= 0) {
|
|
|
|
panes = ['viewer'];
|
|
|
|
} else if (panes.indexOf('viewer') >= 0) {
|
|
|
|
panes = ['editor', 'viewer'];
|
|
|
|
} else {
|
|
|
|
panes = ['editor', 'viewer'];
|
|
|
|
}
|
|
|
|
|
|
|
|
newState = Object.assign({}, state);
|
|
|
|
newState.noteVisiblePanes = panes;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'NOTE_VISIBLE_PANES_SET':
|
|
|
|
|
|
|
|
newState = Object.assign({}, state);
|
|
|
|
newState.noteVisiblePanes = action.panes;
|
|
|
|
break;
|
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
}
|
2017-11-04 15:23:15 +02:00
|
|
|
} catch (error) {
|
2017-11-06 20:35:04 +02:00
|
|
|
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
|
2017-11-04 15:23:15 +02:00
|
|
|
throw error;
|
|
|
|
}
|
2017-11-06 20:35:04 +02:00
|
|
|
|
|
|
|
return super.reducer(newState, action);
|
|
|
|
}
|
|
|
|
|
2017-11-10 20:58:00 +02:00
|
|
|
async generalMiddleware(store, next, action) {
|
2017-11-12 02:44:26 +02:00
|
|
|
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
|
|
|
|
setLocale(Setting.value('locale'));
|
|
|
|
this.refreshMenu();
|
|
|
|
}
|
|
|
|
|
2017-11-10 20:58:00 +02:00
|
|
|
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
2017-11-24 20:06:30 +02:00
|
|
|
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync();
|
2017-11-12 19:02:20 +02:00
|
|
|
}
|
2017-11-10 20:58:00 +02:00
|
|
|
|
2017-11-28 00:50:46 +02:00
|
|
|
if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
|
|
|
|
await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
|
|
|
|
}
|
|
|
|
|
2017-11-11 19:36:47 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-11-12 19:02:20 +02:00
|
|
|
if (['NOTE_VISIBLE_PANES_TOGGLE', 'NOTE_VISIBLE_PANES_SET'].indexOf(action.type) >= 0) {
|
|
|
|
Setting.setValue('noteVisiblePanes', newState.noteVisiblePanes);
|
|
|
|
}
|
|
|
|
|
2017-11-11 19:36:47 +02:00
|
|
|
return result;
|
2017-11-12 02:44:26 +02:00
|
|
|
}
|
2017-11-11 19:36:47 +02:00
|
|
|
|
2017-11-12 02:44:26 +02:00
|
|
|
refreshMenu() {
|
|
|
|
const screen = this.lastMenuScreen_;
|
|
|
|
this.lastMenuScreen_ = null;
|
|
|
|
this.updateMenu(screen);
|
2017-11-10 20:58:00 +02:00
|
|
|
}
|
|
|
|
|
2017-11-11 19:36:47 +02:00
|
|
|
updateMenu(screen) {
|
|
|
|
if (this.lastMenuScreen_ === screen) return;
|
|
|
|
|
2017-11-11 14:00:37 +02:00
|
|
|
const template = [
|
|
|
|
{
|
2017-11-12 02:44:26 +02:00
|
|
|
label: _('File'),
|
2017-11-11 14:00:37 +02:00
|
|
|
submenu: [{
|
2017-11-11 19:36:47 +02:00
|
|
|
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',
|
|
|
|
}, {
|
2017-11-11 14:00:37 +02:00
|
|
|
label: _('Import Evernote notes'),
|
2017-11-11 19:36:47 +02:00
|
|
|
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',
|
2017-11-11 14:00:37 +02:00
|
|
|
}, {
|
|
|
|
label: _('Quit'),
|
|
|
|
accelerator: 'CommandOrControl+Q',
|
|
|
|
click: () => { bridge().electronApp().exit() }
|
|
|
|
}]
|
2017-11-17 20:57:27 +02:00
|
|
|
}, {
|
|
|
|
label: _('Edit'),
|
|
|
|
submenu: [{
|
|
|
|
label: _('Copy'),
|
2017-12-01 21:15:46 +02:00
|
|
|
screens: ['Main', 'OneDriveLogin'],
|
2017-11-17 20:57:27 +02:00
|
|
|
role: 'copy',
|
|
|
|
accelerator: 'CommandOrControl+C',
|
|
|
|
}, {
|
|
|
|
label: _('Cut'),
|
2017-12-01 21:15:46 +02:00
|
|
|
screens: ['Main', 'OneDriveLogin'],
|
2017-11-30 20:44:37 +02:00
|
|
|
role: 'cut',
|
2017-11-17 20:57:27 +02:00
|
|
|
accelerator: 'CommandOrControl+X',
|
|
|
|
}, {
|
|
|
|
label: _('Paste'),
|
2017-12-01 21:15:46 +02:00
|
|
|
screens: ['Main', 'OneDriveLogin'],
|
2017-11-30 20:44:37 +02:00
|
|
|
role: 'paste',
|
2017-11-17 20:57:27 +02:00
|
|
|
accelerator: 'CommandOrControl+V',
|
|
|
|
}, {
|
|
|
|
type: 'separator',
|
2017-12-01 21:15:46 +02:00
|
|
|
screens: ['Main'],
|
2017-11-17 20:57:27 +02:00
|
|
|
}, {
|
|
|
|
label: _('Search in all the notes'),
|
|
|
|
screens: ['Main'],
|
|
|
|
accelerator: 'F6',
|
|
|
|
click: () => {
|
|
|
|
this.dispatch({
|
|
|
|
type: 'WINDOW_COMMAND',
|
|
|
|
name: 'search',
|
|
|
|
});
|
|
|
|
},
|
|
|
|
}]
|
2017-11-11 14:00:37 +02:00
|
|
|
}, {
|
2017-11-12 02:44:26 +02:00
|
|
|
label: _('Tools'),
|
|
|
|
submenu: [{
|
|
|
|
label: _('Options'),
|
|
|
|
click: () => {
|
|
|
|
this.dispatch({
|
|
|
|
type: 'NAV_GO',
|
|
|
|
routeName: 'Config',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}]
|
|
|
|
}, {
|
|
|
|
label: _('Help'),
|
2017-11-11 14:00:37 +02:00
|
|
|
submenu: [{
|
2017-11-14 20:02:58 +02:00
|
|
|
label: _('Website and documentation'),
|
2017-11-11 14:00:37 +02:00
|
|
|
accelerator: 'F1',
|
|
|
|
click () { bridge().openExternal('http://joplin.cozic.net') }
|
|
|
|
}, {
|
|
|
|
label: _('About Joplin'),
|
2017-11-12 02:44:26 +02:00
|
|
|
click: () => {
|
2017-11-28 21:53:29 +02:00
|
|
|
const p = packageInfo;
|
2017-11-12 02:44:26 +02:00
|
|
|
let message = [
|
|
|
|
p.description,
|
|
|
|
'',
|
2017-11-16 13:37:46 +02:00
|
|
|
'Copyright © 2016-2017 Laurent Cozic',
|
2017-11-14 20:02:58 +02:00
|
|
|
_('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform),
|
2017-11-12 02:44:26 +02:00
|
|
|
];
|
|
|
|
bridge().showMessageBox({
|
|
|
|
message: message.join('\n'),
|
|
|
|
});
|
|
|
|
}
|
2017-11-11 14:00:37 +02:00
|
|
|
}]
|
|
|
|
},
|
2017-11-11 19:36:47 +02:00
|
|
|
];
|
|
|
|
|
2017-12-01 21:15:46 +02:00
|
|
|
function isEmptyMenu(template) {
|
|
|
|
for (let i = 0; i < template.length; i++) {
|
|
|
|
const t = template[i];
|
|
|
|
if (t.type !== 'separator') return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-11-11 19:36:47 +02:00
|
|
|
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);
|
2017-12-01 21:15:46 +02:00
|
|
|
if (('submenu' in t) && isEmptyMenu(t.submenu)) continue;
|
2017-11-11 19:36:47 +02:00
|
|
|
output.push(t);
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
let screenTemplate = removeUnwantedItems(template, screen);
|
|
|
|
|
|
|
|
const menu = Menu.buildFromTemplate(screenTemplate);
|
|
|
|
Menu.setApplicationMenu(menu);
|
2017-11-11 14:00:37 +02:00
|
|
|
|
2017-11-11 19:36:47 +02:00
|
|
|
this.lastMenuScreen_ = screen;
|
2017-11-11 14:00:37 +02:00
|
|
|
}
|
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
async start(argv) {
|
|
|
|
argv = await super.start(argv);
|
|
|
|
|
2017-11-28 21:53:29 +02:00
|
|
|
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
2017-11-28 00:50:46 +02:00
|
|
|
AlarmService.setLogger(reg.logger());
|
|
|
|
|
2017-12-01 19:47:18 +02:00
|
|
|
reg.setShowErrorMessageBoxHandler((message) => { bridge().showErrorMessageBox(message) });
|
|
|
|
|
2017-11-17 20:02:01 +02:00
|
|
|
if (Setting.value('openDevTools')) {
|
|
|
|
bridge().window().webContents.openDevTools();
|
|
|
|
}
|
|
|
|
|
2017-11-11 19:36:47 +02:00
|
|
|
this.updateMenu('Main');
|
2017-11-11 14:00:37 +02:00
|
|
|
|
2017-11-06 20:35:04 +02:00
|
|
|
this.initRedux();
|
|
|
|
|
|
|
|
// Since the settings need to be loaded before the store is created, it will never
|
2017-11-08 23:22:24 +02:00
|
|
|
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
|
2017-11-06 20:35:04 +02:00
|
|
|
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
|
|
|
Setting.dispatchUpdateAll();
|
|
|
|
|
|
|
|
await FoldersScreenUtils.refreshFolders();
|
|
|
|
|
|
|
|
const tags = await Tag.allWithNotes();
|
|
|
|
|
|
|
|
this.dispatch({
|
2017-11-08 23:22:24 +02:00
|
|
|
type: 'TAG_UPDATE_ALL',
|
2017-11-06 20:35:04 +02:00
|
|
|
tags: tags,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.store().dispatch({
|
2017-11-08 23:22:24 +02:00
|
|
|
type: 'FOLDER_SELECT',
|
2017-11-06 20:35:04 +02:00
|
|
|
id: Setting.value('activeFolderId'),
|
|
|
|
});
|
2017-11-14 12:18:18 +02:00
|
|
|
|
2017-11-14 20:02:58 +02:00
|
|
|
// Note: Auto-update currently doesn't work in Linux: it downloads the update
|
|
|
|
// but then doesn't install it on exit.
|
|
|
|
if (shim.isWindows() || shim.isMac()) {
|
|
|
|
const runAutoUpdateCheck = function() {
|
2017-11-21 21:37:34 +02:00
|
|
|
if (Setting.value('autoUpdateEnabled')) {
|
|
|
|
bridge().checkForUpdatesAndNotify(Setting.value('profileDir') + '/log-autoupdater.txt');
|
|
|
|
}
|
2017-11-14 20:02:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
setTimeout(() => { runAutoUpdateCheck() }, 5000);
|
|
|
|
// For those who leave the app always open
|
|
|
|
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
|
2017-11-14 12:53:18 +02:00
|
|
|
}
|
2017-11-21 21:47:29 +02:00
|
|
|
|
2017-11-28 02:22:38 +02:00
|
|
|
setTimeout(() => {
|
|
|
|
AlarmService.garbageCollect();
|
|
|
|
}, 1000 * 60 * 60);
|
|
|
|
|
2017-11-30 20:36:26 +02:00
|
|
|
if (Setting.value('env') === 'dev') {
|
2017-11-28 02:22:38 +02:00
|
|
|
AlarmService.updateAllNotifications();
|
2017-11-30 20:36:26 +02:00
|
|
|
} else {
|
|
|
|
reg.scheduleSync().then(() => {
|
|
|
|
// Wait for the first sync before updating the notifications, since synchronisation
|
|
|
|
// might change the notifications.
|
|
|
|
AlarmService.updateAllNotifications();
|
|
|
|
});
|
|
|
|
}
|
2017-11-04 14:23:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let application_ = null;
|
|
|
|
|
|
|
|
function app() {
|
2017-11-05 02:17:48 +02:00
|
|
|
if (!application_) application_ = new Application();
|
2017-11-04 14:23:46 +02:00
|
|
|
return application_;
|
|
|
|
}
|
|
|
|
|
2017-11-11 14:00:37 +02:00
|
|
|
module.exports = { app };
|