mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-15 09:04:04 +02:00
cb8dca747b
Refactor note editor using React Hooks and TypeScript and moved editor-specific code to separate files. Moved business logic into more maintainable custom hooks. Squashed commit of the following: commit f243d9bf89bdcfa1849ee26df5c0dd3e33405010 Author: Laurent Cozic <laurent@cozic.net> Date: Sat May 2 16:04:14 2020 +0100 Fixed saving issue commit 055f68d2e8b6cf6f130336c38ac2ab480887583d Author: Laurent Cozic <laurent@cozic.net> Date: Sat May 2 15:43:38 2020 +0100 Fixed HTML notes commit 99a3cf71f58d2fedcdf3001bf4110b6e8e3993da Merge: 9be85c45f2b16ebbbf7a
Author: Laurent Cozic <laurent@cozic.net> Date: Sat May 2 12:54:42 2020 +0100 Merge branch 'master' into refactor_note_text commit 9be85c45f23e5cb1ecd612b0ee631947871ada6f Author: Laurent Cozic <laurent@cozic.net> Date: Sat May 2 12:21:01 2020 +0100 Ident to space commit 848dde1869c010fe5851f493ef7287ada5f2991e Author: Laurent Cozic <laurent@cozic.net> Date: Sat May 2 11:28:50 2020 +0100 Refactor prop types commit 13c3bbe2b4f9a522ea3f8a25e7e5e7bb026dfd4f Author: Laurent Cozic <laurent@cozic.net> Date: Sat May 2 11:15:45 2020 +0100 Fixed resource loading issue commit 50cb38e3f00ef40ea8b6a468eadd66728a3ec332 Author: Laurent Cozic <laurent@cozic.net> Date: Fri May 1 23:46:58 2020 +0100 Fixed resource loading logic commit bc42ed03735f50c8394d597bb9e67312e55752fe Author: Laurent Cozic <laurent@cozic.net> Date: Fri May 1 23:08:41 2020 +0100 Various fixes commit 03c038e6d6cbde03bd474798b96c4eb120fd1647 Author: Laurent Cozic <laurent@cozic.net> Date: Wed Apr 29 23:22:49 2020 +0100 Fixed resource handling commit dc6c15302fac094c4e7dec5a20c9fcc4edb3d132 Author: Laurent Cozic <laurent@cozic.net> Date: Wed Apr 29 22:55:13 2020 +0100 Moved more code to files commit 398d5121e53df34de89b4148ef2cfd3a7bbe4feb Author: Laurent Cozic <laurent@cozic.net> Date: Wed Apr 29 00:22:43 2020 +0000 More fixes commit 3ebbb80147d7d502fd955776c7fedb743400597f Author: Laurent Cozic <laurent@cozic.net> Date: Wed Apr 29 00:12:44 2020 +0000 Various improvements and bug fixes commit 52a65ed3875e0709117ca93ba723e20624577d05 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Apr 28 23:51:07 2020 +0000 Move more code to sub-files commit 33ccf530fb442d7ddae0852cbab2c335efdbbf33 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Apr 28 23:25:12 2020 +0100 Moved code to sub-files commit ba3ad2cf9fcc1d7809df4afe93cd9737585a9960 Merge: 445acdab73150ee14de6
Author: Laurent Cozic <laurent@cozic.net> Date: Tue Apr 28 22:28:56 2020 +0100 Merge branch 'master' into refactor_note_text commit 445acdab7368345369d7f69b9becd1e77c8383dc Author: Laurent Cozic <laurent@cozic.net> Date: Tue Apr 28 19:01:41 2020 +0100 Imported more code commit 772481d3a3ac7f0b0b00e86394c0f4fd2f3a9fa7 Author: Laurent Cozic <laurent@cozic.net> Date: Mon Apr 27 23:43:17 2020 +0000 Handle save/load state commit b3b92192ae3a1a30e3018810346cebfad47ac5e3 Author: Laurent Cozic <laurent@cozic.net> Date: Mon Apr 27 23:11:11 2020 +0000 Clean up and added back scroll commit 7a19ecfd0cb7fef1d58ece2e024099c7e40986da Author: Laurent Cozic <laurent@cozic.net> Date: Mon Apr 27 22:29:39 2020 +0100 More refactoring commit ac388afd381eaecfa4582b3566d032c9d953c4dc Author: Laurent Cozic <laurent@cozic.net> Date: Sun Apr 26 17:07:01 2020 +0100 Restored print commit 1d2c0ed389a5398dacc584d24922c5ea0dda861a Author: Laurent Cozic <laurent@cozic.net> Date: Sun Apr 26 12:03:15 2020 +0100 Put back search commit c618cb59d43fa3bb507dbd0b757b302ecfe907b3 Author: Laurent Cozic <laurent@cozic.net> Date: Sat Apr 25 18:21:11 2020 +0100 Restore scrolling behaviour commit 324e6ea79ebafab1d2bca246ef030751147a47eb Author: Laurent Cozic <laurent@cozic.net> Date: Sat Apr 25 10:22:31 2020 +0100 Simplified saving notes commit ef089aaf2289193bf275d94c1f2785f6d88657e4 Author: Laurent Cozic <laurent@cozic.net> Date: Sat Apr 25 10:12:16 2020 +0100 More refactoring commit 61b102307d5a98d2c1502d7bf073592da21af720 Author: Laurent Cozic <laurent@cozic.net> Date: Fri Apr 24 18:04:44 2020 +0100 Added back note revisions commit 7d5e3694d0df044b8493d9114e89e2d81c9b69ad Author: Laurent Cozic <laurent@cozic.net> Date: Thu Apr 23 22:51:52 2020 +0000 More note toolbar refactoring commit a56d58e7c80d91f29afadaffaaa004f3254482f7 Author: Laurent Cozic <laurent@cozic.net> Date: Thu Apr 23 20:54:37 2020 +0100 Finished toolbar refactoring commit 6c8ef9f44f880a9569eed5c54c9c47dca2251e5e Author: Laurent Cozic <laurent@cozic.net> Date: Thu Apr 23 19:17:44 2020 +0100 More refactoring commit 7de8057158a9256e2e0dcf948081e10a6a642216 Author: Laurent Cozic <laurent@cozic.net> Date: Wed Apr 22 23:48:42 2020 +0100 Started refactoring commands commit 177263c85e7d17d8ddc01b583738c2ab14b3acd7 Merge: f58f1a06e07ceb68d835
Author: Laurent Cozic <laurent@cozic.net> Date: Wed Apr 22 20:26:19 2020 +0100 Merge branch 'master' into refactor_note_text commit f58f1a06e08b3cf80e2ac7a794b15f4b5caf8932 Author: Laurent Cozic <laurent@cozic.net> Date: Wed Apr 22 20:25:43 2020 +0100 Moving Ace Editor to separate component commit a83d3a220515137985c0f334f5848c91b8539138 Author: Laurent Cozic <laurent@cozic.net> Date: Mon Apr 20 20:33:21 2020 +0000 Cleaned up directory structure for note editor commit c6f2e609c9443bac21de5033bbedf86ac6f12cc0 Author: Laurent Cozic <laurent@cozic.net> Date: Mon Apr 20 19:23:06 2020 +0100 Added "note" menu to move note-related items to it commit 1219465318ae5a7a2c777ae2ec15d3357e1499df Author: Laurent Cozic <laurent@cozic.net> Date: Mon Apr 20 19:05:04 2020 +0100 Moved note related toolbar to separate component
1461 lines
39 KiB
JavaScript
1461 lines
39 KiB
JavaScript
require('app-module-path').addPath(__dirname);
|
|
|
|
const { BaseApplication } = require('lib/BaseApplication');
|
|
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
|
const Setting = require('lib/models/Setting.js');
|
|
const { shim } = require('lib/shim.js');
|
|
const MasterKey = require('lib/models/MasterKey');
|
|
const Note = require('lib/models/Note');
|
|
const { MarkupToHtml } = require('lib/joplin-renderer');
|
|
const { _, setLocale } = require('lib/locale.js');
|
|
const { Logger } = require('lib/logger.js');
|
|
const fs = require('fs-extra');
|
|
const Tag = require('lib/models/Tag.js');
|
|
const { reg } = require('lib/registry.js');
|
|
const { defaultState } = require('lib/reducer.js');
|
|
const packageInfo = require('./packageInfo.js');
|
|
const AlarmService = require('lib/services/AlarmService.js');
|
|
const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode');
|
|
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
|
const InteropService = require('lib/services/InteropService');
|
|
const InteropServiceHelper = require('./InteropServiceHelper.js');
|
|
const ResourceService = require('lib/services/ResourceService');
|
|
const ClipperServer = require('lib/ClipperServer');
|
|
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
|
const { bridge } = require('electron').remote.require('./bridge');
|
|
const { shell, webFrame, clipboard } = require('electron');
|
|
const Menu = bridge().Menu;
|
|
const PluginManager = require('lib/services/PluginManager');
|
|
const RevisionService = require('lib/services/RevisionService');
|
|
const MigrationService = require('lib/services/MigrationService');
|
|
const TemplateUtils = require('lib/TemplateUtils');
|
|
const CssUtils = require('lib/CssUtils');
|
|
|
|
const pluginClasses = [
|
|
require('./plugins/GotoAnything.min'),
|
|
];
|
|
|
|
const appDefaultState = Object.assign({}, defaultState, {
|
|
route: {
|
|
type: 'NAV_GO',
|
|
routeName: 'Main',
|
|
props: {},
|
|
},
|
|
navHistory: [],
|
|
fileToImport: null,
|
|
windowCommand: null,
|
|
noteVisiblePanes: ['editor', 'viewer'],
|
|
sidebarVisibility: true,
|
|
noteListVisibility: true,
|
|
windowContentSize: bridge().windowContentSize(),
|
|
watchedNoteFiles: [],
|
|
lastEditorScrollPercents: {},
|
|
devToolsVisible: false,
|
|
});
|
|
|
|
class Application extends BaseApplication {
|
|
|
|
constructor() {
|
|
super();
|
|
this.lastMenuScreen_ = null;
|
|
}
|
|
|
|
hasGui() {
|
|
return true;
|
|
}
|
|
|
|
checkForUpdateLoggerPath() {
|
|
return `${Setting.value('profileDir')}/log-autoupdater.txt`;
|
|
}
|
|
|
|
reducer(state = appDefaultState, action) {
|
|
let newState = state;
|
|
|
|
try {
|
|
switch (action.type) {
|
|
|
|
case 'NAV_BACK':
|
|
case 'NAV_GO':
|
|
|
|
{
|
|
const goingBack = action.type === 'NAV_BACK';
|
|
|
|
if (goingBack && !state.navHistory.length) break;
|
|
|
|
const currentRoute = state.route;
|
|
|
|
newState = Object.assign({}, state);
|
|
const newNavHistory = state.navHistory.slice();
|
|
|
|
if (goingBack) {
|
|
let newAction = null;
|
|
while (newNavHistory.length) {
|
|
newAction = newNavHistory.pop();
|
|
if (newAction.routeName !== state.route.routeName) break;
|
|
}
|
|
|
|
if (!newAction) break;
|
|
|
|
action = newAction;
|
|
}
|
|
|
|
if (!goingBack) newNavHistory.push(currentRoute);
|
|
newState.navHistory = newNavHistory;
|
|
newState.route = action;
|
|
}
|
|
break;
|
|
|
|
case 'WINDOW_CONTENT_SIZE_SET':
|
|
|
|
newState = Object.assign({}, state);
|
|
newState.windowContentSize = action.size;
|
|
break;
|
|
|
|
case 'WINDOW_COMMAND':
|
|
|
|
{
|
|
newState = Object.assign({}, state);
|
|
const command = Object.assign({}, action);
|
|
delete command.type;
|
|
newState.windowCommand = command.name ? command : null;
|
|
}
|
|
break;
|
|
|
|
case 'NOTE_VISIBLE_PANES_TOGGLE':
|
|
|
|
{
|
|
const getNextLayout = (currentLayout) => {
|
|
currentLayout = panes.length === 2 ? 'both' : currentLayout[0];
|
|
|
|
let paneOptions;
|
|
if (state.settings.layoutButtonSequence === Setting.LAYOUT_EDITOR_VIEWER) {
|
|
paneOptions = ['editor', 'viewer'];
|
|
} else if (state.settings.layoutButtonSequence === Setting.LAYOUT_EDITOR_SPLIT) {
|
|
paneOptions = ['editor', 'both'];
|
|
} else if (state.settings.layoutButtonSequence === Setting.LAYOUT_VIEWER_SPLIT) {
|
|
paneOptions = ['viewer', 'both'];
|
|
} else {
|
|
paneOptions = ['editor', 'viewer', 'both'];
|
|
}
|
|
|
|
const currentLayoutIndex = paneOptions.indexOf(currentLayout);
|
|
const nextLayoutIndex = currentLayoutIndex === paneOptions.length - 1 ? 0 : currentLayoutIndex + 1;
|
|
|
|
const nextLayout = paneOptions[nextLayoutIndex];
|
|
return nextLayout === 'both' ? ['editor', 'viewer'] : [nextLayout];
|
|
};
|
|
|
|
newState = Object.assign({}, state);
|
|
|
|
const panes = state.noteVisiblePanes.slice();
|
|
newState.noteVisiblePanes = getNextLayout(panes);
|
|
}
|
|
break;
|
|
|
|
case 'NOTE_VISIBLE_PANES_SET':
|
|
|
|
newState = Object.assign({}, state);
|
|
newState.noteVisiblePanes = action.panes;
|
|
break;
|
|
|
|
case 'SIDEBAR_VISIBILITY_TOGGLE':
|
|
|
|
newState = Object.assign({}, state);
|
|
newState.sidebarVisibility = !state.sidebarVisibility;
|
|
break;
|
|
|
|
case 'SIDEBAR_VISIBILITY_SET':
|
|
newState = Object.assign({}, state);
|
|
newState.sidebarVisibility = action.visibility;
|
|
break;
|
|
|
|
case 'NOTELIST_VISIBILITY_TOGGLE':
|
|
newState = Object.assign({}, state);
|
|
newState.noteListVisibility = !state.noteListVisibility;
|
|
break;
|
|
|
|
case 'NOTELIST_VISIBILITY_SET':
|
|
newState = Object.assign({}, state);
|
|
newState.noteListVisibility = action.visibility;
|
|
break;
|
|
|
|
case 'NOTE_FILE_WATCHER_ADD':
|
|
|
|
if (newState.watchedNoteFiles.indexOf(action.id) < 0) {
|
|
newState = Object.assign({}, state);
|
|
const watchedNoteFiles = newState.watchedNoteFiles.slice();
|
|
watchedNoteFiles.push(action.id);
|
|
newState.watchedNoteFiles = watchedNoteFiles;
|
|
}
|
|
break;
|
|
|
|
case 'NOTE_FILE_WATCHER_REMOVE':
|
|
|
|
{
|
|
newState = Object.assign({}, state);
|
|
const idx = newState.watchedNoteFiles.indexOf(action.id);
|
|
if (idx >= 0) {
|
|
const watchedNoteFiles = newState.watchedNoteFiles.slice();
|
|
watchedNoteFiles.splice(idx, 1);
|
|
newState.watchedNoteFiles = watchedNoteFiles;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'NOTE_FILE_WATCHER_CLEAR':
|
|
|
|
if (state.watchedNoteFiles.length) {
|
|
newState = Object.assign({}, state);
|
|
newState.watchedNoteFiles = [];
|
|
}
|
|
break;
|
|
|
|
case 'EDITOR_SCROLL_PERCENT_SET':
|
|
|
|
{
|
|
newState = Object.assign({}, state);
|
|
const newPercents = Object.assign({}, newState.lastEditorScrollPercents);
|
|
newPercents[action.noteId] = action.percent;
|
|
newState.lastEditorScrollPercents = newPercents;
|
|
}
|
|
break;
|
|
|
|
case 'NOTE_DEVTOOLS_TOGGLE':
|
|
newState = Object.assign({}, state);
|
|
newState.devToolsVisible = !newState.devToolsVisible;
|
|
break;
|
|
|
|
case 'NOTE_DEVTOOLS_SET':
|
|
newState = Object.assign({}, state);
|
|
newState.devToolsVisible = action.value;
|
|
break;
|
|
|
|
}
|
|
} catch (error) {
|
|
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
|
throw error;
|
|
}
|
|
|
|
return super.reducer(newState, action);
|
|
}
|
|
|
|
toggleDevTools(visible) {
|
|
if (visible) {
|
|
bridge().openDevTools();
|
|
} else {
|
|
bridge().closeDevTools();
|
|
}
|
|
}
|
|
|
|
async generalMiddleware(store, next, action) {
|
|
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
|
|
setLocale(Setting.value('locale'));
|
|
// The bridge runs within the main process, with its own instance of locale.js
|
|
// so it needs to be set too here.
|
|
bridge().setLocale(Setting.value('locale'));
|
|
await this.refreshMenu();
|
|
}
|
|
|
|
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
|
|
this.updateTray();
|
|
}
|
|
|
|
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'style.editor.fontFamily' || action.type == 'SETTING_UPDATE_ALL') {
|
|
this.updateEditorFont();
|
|
}
|
|
|
|
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'windowContentZoomFactor' || action.type == 'SETTING_UPDATE_ALL') {
|
|
webFrame.setZoomFactor(Setting.value('windowContentZoomFactor') / 100);
|
|
}
|
|
|
|
if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
|
|
await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if (['NOTE_VISIBLE_PANES_TOGGLE', 'NOTE_VISIBLE_PANES_SET'].indexOf(action.type) >= 0) {
|
|
Setting.setValue('noteVisiblePanes', newState.noteVisiblePanes);
|
|
}
|
|
|
|
if (['SIDEBAR_VISIBILITY_TOGGLE', 'SIDEBAR_VISIBILITY_SET'].indexOf(action.type) >= 0) {
|
|
Setting.setValue('sidebarVisibility', newState.sidebarVisibility);
|
|
}
|
|
|
|
if (['NOTELIST_VISIBILITY_TOGGLE', 'NOTELIST_VISIBILITY_SET'].indexOf(action.type) >= 0) {
|
|
Setting.setValue('noteListVisibility', newState.noteListVisibility);
|
|
}
|
|
|
|
if (action.type.indexOf('NOTE_SELECT') === 0 || action.type.indexOf('FOLDER_SELECT') === 0) {
|
|
this.updateMenuItemStates(newState);
|
|
}
|
|
|
|
if (['NOTE_DEVTOOLS_TOGGLE', 'NOTE_DEVTOOLS_SET'].indexOf(action.type) >= 0) {
|
|
this.toggleDevTools(newState.devToolsVisible);
|
|
this.updateMenuItemStates(newState);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async refreshMenu() {
|
|
const screen = this.lastMenuScreen_;
|
|
this.lastMenuScreen_ = null;
|
|
await this.updateMenu(screen);
|
|
}
|
|
|
|
focusElement_(target) {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'focusElement',
|
|
target: target,
|
|
});
|
|
}
|
|
|
|
async updateMenu(screen) {
|
|
if (this.lastMenuScreen_ === screen) return;
|
|
|
|
const sortNoteFolderItems = (type) => {
|
|
const sortItems = [];
|
|
const sortOptions = Setting.enumOptions(`${type}.sortOrder.field`);
|
|
for (const field in sortOptions) {
|
|
if (!sortOptions.hasOwnProperty(field)) continue;
|
|
sortItems.push({
|
|
label: sortOptions[field],
|
|
screens: ['Main'],
|
|
type: 'checkbox',
|
|
checked: Setting.value(`${type}.sortOrder.field`) === field,
|
|
click: () => {
|
|
Setting.setValue(`${type}.sortOrder.field`, field);
|
|
this.refreshMenu();
|
|
},
|
|
});
|
|
}
|
|
|
|
sortItems.push({ type: 'separator' });
|
|
|
|
sortItems.push({
|
|
label: Setting.settingMetadata(`${type}.sortOrder.reverse`).label(),
|
|
type: 'checkbox',
|
|
checked: Setting.value(`${type}.sortOrder.reverse`),
|
|
screens: ['Main'],
|
|
click: () => {
|
|
Setting.setValue(`${type}.sortOrder.reverse`, !Setting.value(`${type}.sortOrder.reverse`));
|
|
},
|
|
});
|
|
|
|
return sortItems;
|
|
};
|
|
|
|
const sortNoteItems = sortNoteFolderItems('notes');
|
|
const sortFolderItems = sortNoteFolderItems('folders');
|
|
|
|
const focusItems = [];
|
|
|
|
focusItems.push({
|
|
label: _('Sidebar'),
|
|
click: () => { this.focusElement_('sideBar'); },
|
|
accelerator: 'CommandOrControl+Shift+S',
|
|
});
|
|
|
|
focusItems.push({
|
|
label: _('Note list'),
|
|
click: () => { this.focusElement_('noteList'); },
|
|
accelerator: 'CommandOrControl+Shift+L',
|
|
});
|
|
|
|
focusItems.push({
|
|
label: _('Note title'),
|
|
click: () => { this.focusElement_('noteTitle'); },
|
|
accelerator: 'CommandOrControl+Shift+N',
|
|
});
|
|
|
|
focusItems.push({
|
|
label: _('Note body'),
|
|
click: () => { this.focusElement_('noteBody'); },
|
|
accelerator: 'CommandOrControl+Shift+B',
|
|
});
|
|
|
|
let toolsItems = [];
|
|
const importItems = [];
|
|
const exportItems = [];
|
|
const toolsItemsFirst = [];
|
|
const templateItems = [];
|
|
const ioService = new InteropService();
|
|
const ioModules = ioService.modules();
|
|
for (let i = 0; i < ioModules.length; i++) {
|
|
const module = ioModules[i];
|
|
if (module.type === 'exporter') {
|
|
if (module.canDoMultiExport !== false) {
|
|
exportItems.push({
|
|
label: module.fullLabel(),
|
|
screens: ['Main'],
|
|
click: async () => {
|
|
await InteropServiceHelper.export(this.dispatch.bind(this), module);
|
|
},
|
|
});
|
|
}
|
|
} else {
|
|
for (let j = 0; j < module.sources.length; j++) {
|
|
const moduleSource = module.sources[j];
|
|
importItems.push({
|
|
label: module.fullLabel(moduleSource),
|
|
screens: ['Main'],
|
|
click: async () => {
|
|
let path = null;
|
|
|
|
const selectedFolderId = this.store().getState().selectedFolderId;
|
|
|
|
if (moduleSource === 'file') {
|
|
path = bridge().showOpenDialog({
|
|
filters: [{ name: module.description, extensions: module.fileExtensions }],
|
|
});
|
|
} else {
|
|
path = bridge().showOpenDialog({
|
|
properties: ['openDirectory', 'createDirectory'],
|
|
});
|
|
}
|
|
|
|
if (!path || (Array.isArray(path) && !path.length)) return;
|
|
|
|
if (Array.isArray(path)) path = path[0];
|
|
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'showModalMessage',
|
|
message: _('Importing from "%s" as "%s" format. Please wait...', path, module.format),
|
|
});
|
|
|
|
const importOptions = {
|
|
path,
|
|
format: module.format,
|
|
modulePath: module.path,
|
|
onError: console.warn,
|
|
destinationFolderId:
|
|
!module.isNoteArchive && moduleSource === 'file'
|
|
? selectedFolderId
|
|
: null,
|
|
};
|
|
|
|
const service = new InteropService();
|
|
try {
|
|
const result = await service.import(importOptions);
|
|
console.info('Import result: ', result);
|
|
} catch (error) {
|
|
bridge().showErrorMessageBox(error.message);
|
|
}
|
|
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'hideModalMessage',
|
|
});
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
exportItems.push({
|
|
label: `PDF - ${_('PDF File')}`,
|
|
screens: ['Main'],
|
|
click: async () => {
|
|
const selectedNoteIds = this.store().getState().selectedNoteIds;
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'exportPdf',
|
|
noteIds: selectedNoteIds,
|
|
});
|
|
},
|
|
});
|
|
|
|
// We need a dummy entry, otherwise the ternary operator to show a
|
|
// menu item only on a specific OS does not work.
|
|
const noItem = {
|
|
type: 'separator',
|
|
visible: false,
|
|
};
|
|
|
|
const syncStatusItem = {
|
|
label: _('Synchronisation Status'),
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'NAV_GO',
|
|
routeName: 'Status',
|
|
});
|
|
},
|
|
};
|
|
|
|
const newNoteItem = {
|
|
label: _('New note'),
|
|
accelerator: 'CommandOrControl+N',
|
|
screens: ['Main'],
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'newNote',
|
|
});
|
|
},
|
|
};
|
|
|
|
const newTodoItem = {
|
|
label: _('New to-do'),
|
|
accelerator: 'CommandOrControl+T',
|
|
screens: ['Main'],
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'newTodo',
|
|
});
|
|
},
|
|
};
|
|
|
|
const newNotebookItem = {
|
|
label: _('New notebook'),
|
|
screens: ['Main'],
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'newNotebook',
|
|
});
|
|
},
|
|
};
|
|
|
|
const newSubNotebookItem = {
|
|
label: _('New sub-notebook'),
|
|
screens: ['Main'],
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'newSubNotebook',
|
|
activeFolderId: Setting.value('activeFolderId'),
|
|
});
|
|
},
|
|
};
|
|
|
|
const printItem = {
|
|
label: _('Print'),
|
|
accelerator: 'CommandOrControl+P',
|
|
screens: ['Main'],
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'print',
|
|
noteIds: this.store().getState().selectedNoteIds,
|
|
});
|
|
},
|
|
};
|
|
|
|
toolsItemsFirst.push(syncStatusItem, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
});
|
|
|
|
const templateDirExists = await shim.fsDriver().exists(Setting.value('templateDir'));
|
|
|
|
templateItems.push({
|
|
label: _('Create note from template'),
|
|
visible: templateDirExists,
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'selectTemplate',
|
|
noteType: 'note',
|
|
});
|
|
},
|
|
}, {
|
|
label: _('Create to-do from template'),
|
|
visible: templateDirExists,
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'selectTemplate',
|
|
noteType: 'todo',
|
|
});
|
|
},
|
|
}, {
|
|
label: _('Insert template'),
|
|
visible: templateDirExists,
|
|
accelerator: 'CommandOrControl+Alt+I',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'selectTemplate',
|
|
});
|
|
},
|
|
}, {
|
|
label: _('Open template directory'),
|
|
click: () => {
|
|
const templateDir = Setting.value('templateDir');
|
|
if (!templateDirExists) shim.fsDriver().mkdir(templateDir);
|
|
shell.openItem(templateDir);
|
|
},
|
|
}, {
|
|
label: _('Refresh templates'),
|
|
click: async () => {
|
|
const templates = await TemplateUtils.loadTemplates(Setting.value('templateDir'));
|
|
|
|
this.store().dispatch({
|
|
type: 'TEMPLATE_UPDATE_ALL',
|
|
templates: templates,
|
|
});
|
|
},
|
|
});
|
|
|
|
// we need this workaround, because on macOS the menu is different
|
|
const toolsItemsWindowsLinux = toolsItemsFirst.concat([{
|
|
label: _('Options'),
|
|
visible: !shim.isMac(),
|
|
accelerator: 'CommandOrControl+,',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'NAV_GO',
|
|
routeName: 'Config',
|
|
});
|
|
},
|
|
}]);
|
|
|
|
// the following menu items will be available for all OS under Tools
|
|
const toolsItemsAll = [{
|
|
label: _('Note attachments...'),
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'NAV_GO',
|
|
routeName: 'Resources',
|
|
});
|
|
},
|
|
}];
|
|
|
|
if (!shim.isMac()) {
|
|
toolsItems = toolsItems.concat(toolsItemsWindowsLinux);
|
|
}
|
|
toolsItems = toolsItems.concat(toolsItemsAll);
|
|
|
|
function _checkForUpdates(ctx) {
|
|
bridge().checkForUpdates(false, bridge().window(), ctx.checkForUpdateLoggerPath(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
|
}
|
|
|
|
function _showAbout() {
|
|
const p = packageInfo;
|
|
let gitInfo = '';
|
|
if ('git' in p) {
|
|
gitInfo = _('Revision: %s (%s)', p.git.hash, p.git.branch);
|
|
}
|
|
const copyrightText = 'Copyright © 2016-YYYY Laurent Cozic';
|
|
const message = [
|
|
p.description,
|
|
'',
|
|
copyrightText.replace('YYYY', new Date().getFullYear()),
|
|
_('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform),
|
|
'',
|
|
_('Client ID: %s', Setting.value('clientId')),
|
|
_('Sync Version: %s', Setting.value('syncVersion')),
|
|
_('Profile Version: %s', reg.db().version()),
|
|
];
|
|
if (gitInfo) {
|
|
message.push(`\n${gitInfo}`);
|
|
console.info(gitInfo);
|
|
}
|
|
const text = message.join('\n');
|
|
|
|
const copyToClipboard = bridge().showMessageBox(text, {
|
|
icon: `${bridge().electronApp().buildDir()}/icons/128x128.png`,
|
|
buttons: [_('Copy'), _('OK')],
|
|
cancelId: 1,
|
|
defaultId: 1,
|
|
});
|
|
if (copyToClipboard === 0) {
|
|
clipboard.writeText(message.splice(3).join('\n'));
|
|
}
|
|
}
|
|
|
|
const rootMenuFile = {
|
|
// Using a dummy entry for macOS here, because first menu
|
|
// becomes 'Joplin' and we need a nenu called 'File' later.
|
|
label: shim.isMac() ? '&JoplinMainMenu' : _('&File'),
|
|
// `&` before one of the char in the label name mean, that
|
|
// <Alt + F> will open this menu. It's needed becase electron
|
|
// opens the first menu on Alt press if no hotkey assigned.
|
|
// Issue: https://github.com/laurent22/joplin/issues/934
|
|
submenu: [{
|
|
label: _('About Joplin'),
|
|
visible: shim.isMac() ? true : false,
|
|
click: () => _showAbout(),
|
|
}, {
|
|
type: 'separator',
|
|
visible: shim.isMac() ? true : false,
|
|
}, {
|
|
label: _('Preferences...'),
|
|
visible: shim.isMac() ? true : false,
|
|
accelerator: 'CommandOrControl+,',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'NAV_GO',
|
|
routeName: 'Config',
|
|
});
|
|
},
|
|
}, {
|
|
label: _('Check for updates...'),
|
|
visible: shim.isMac() ? true : false,
|
|
click: () => _checkForUpdates(this),
|
|
}, {
|
|
type: 'separator',
|
|
visible: shim.isMac() ? true : false,
|
|
},
|
|
shim.isMac() ? noItem : newNoteItem,
|
|
shim.isMac() ? noItem : newTodoItem,
|
|
shim.isMac() ? noItem : newNotebookItem,
|
|
shim.isMac() ? noItem : newSubNotebookItem, {
|
|
type: 'separator',
|
|
visible: shim.isMac() ? false : true,
|
|
}, {
|
|
label: _('Templates'),
|
|
visible: shim.isMac() ? false : true,
|
|
submenu: templateItems,
|
|
}, {
|
|
type: 'separator',
|
|
visible: shim.isMac() ? false : true,
|
|
}, {
|
|
label: _('Import'),
|
|
visible: shim.isMac() ? false : true,
|
|
submenu: importItems,
|
|
}, {
|
|
label: _('Export'),
|
|
visible: shim.isMac() ? false : true,
|
|
submenu: exportItems,
|
|
}, {
|
|
type: 'separator',
|
|
}, {
|
|
label: _('Synchronise'),
|
|
accelerator: 'CommandOrControl+S',
|
|
screens: ['Main'],
|
|
click: async () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'synchronize',
|
|
});
|
|
},
|
|
}, shim.isMac() ? syncStatusItem : noItem, {
|
|
type: 'separator',
|
|
}, shim.isMac() ? noItem : printItem, {
|
|
type: 'separator',
|
|
platforms: ['darwin'],
|
|
}, {
|
|
label: _('Hide %s', 'Joplin'),
|
|
platforms: ['darwin'],
|
|
accelerator: 'CommandOrControl+H',
|
|
click: () => { bridge().electronApp().hide(); },
|
|
}, {
|
|
type: 'separator',
|
|
}, {
|
|
label: _('Quit'),
|
|
accelerator: 'CommandOrControl+Q',
|
|
click: () => { bridge().electronApp().quit(); },
|
|
}],
|
|
};
|
|
|
|
const rootMenuFileMacOs = {
|
|
label: _('&File'),
|
|
visible: shim.isMac() ? true : false,
|
|
submenu: [
|
|
newNoteItem,
|
|
newTodoItem,
|
|
newNotebookItem,
|
|
newSubNotebookItem, {
|
|
label: _('Close Window'),
|
|
platforms: ['darwin'],
|
|
accelerator: 'Command+W',
|
|
selector: 'performClose:',
|
|
}, {
|
|
type: 'separator',
|
|
}, {
|
|
label: _('Templates'),
|
|
submenu: templateItems,
|
|
}, {
|
|
type: 'separator',
|
|
}, {
|
|
label: _('Import'),
|
|
submenu: importItems,
|
|
}, {
|
|
label: _('Export'),
|
|
submenu: exportItems,
|
|
}, {
|
|
type: 'separator',
|
|
},
|
|
printItem,
|
|
],
|
|
};
|
|
|
|
const layoutButtonSequenceOptions = Object.entries(Setting.enumOptions('layoutButtonSequence')).map(([layoutKey, layout]) => ({
|
|
label: layout,
|
|
screens: ['Main'],
|
|
type: 'checkbox',
|
|
checked: Setting.value('layoutButtonSequence') == layoutKey,
|
|
click: () => {
|
|
Setting.setValue('layoutButtonSequence', layoutKey);
|
|
this.refreshMenu();
|
|
},
|
|
}));
|
|
|
|
const rootMenus = {
|
|
edit: {
|
|
id: 'edit',
|
|
label: _('&Edit'),
|
|
submenu: [{
|
|
id: 'edit:copy',
|
|
label: _('Copy'),
|
|
role: 'copy',
|
|
accelerator: 'CommandOrControl+C',
|
|
}, {
|
|
id: 'edit:cut',
|
|
label: _('Cut'),
|
|
role: 'cut',
|
|
accelerator: 'CommandOrControl+X',
|
|
}, {
|
|
id: 'edit:paste',
|
|
label: _('Paste'),
|
|
role: 'paste',
|
|
accelerator: 'CommandOrControl+V',
|
|
}, {
|
|
id: 'edit:selectAll',
|
|
label: _('Select all'),
|
|
role: 'selectall',
|
|
accelerator: 'CommandOrControl+A',
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
id: 'edit:bold',
|
|
label: _('Bold'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+B',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'textBold',
|
|
});
|
|
},
|
|
}, {
|
|
id: 'edit:italic',
|
|
label: _('Italic'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+I',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'textItalic',
|
|
});
|
|
},
|
|
}, {
|
|
id: 'edit:link',
|
|
label: _('Link'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+K',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'textLink',
|
|
});
|
|
},
|
|
}, {
|
|
id: 'edit:code',
|
|
label: _('Code'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+`',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'textCode',
|
|
});
|
|
},
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
id: 'edit:insertDateTime',
|
|
label: _('Insert Date Time'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+Shift+T',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'insertDateTime',
|
|
});
|
|
},
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
id: 'edit:focusSearch',
|
|
label: _('Search in all the notes'),
|
|
screens: ['Main'],
|
|
accelerator: shim.isMac() ? 'Shift+Command+F' : 'F6',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'focusSearch',
|
|
});
|
|
},
|
|
}, {
|
|
id: 'edit:showLocalSearch',
|
|
label: _('Search in current note'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+F',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'showLocalSearch',
|
|
});
|
|
},
|
|
}],
|
|
},
|
|
view: {
|
|
label: _('&View'),
|
|
submenu: [{
|
|
label: _('Toggle sidebar'),
|
|
screens: ['Main'],
|
|
accelerator: shim.isMac() ? 'Option+Command+S' : 'F10',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'toggleSidebar',
|
|
});
|
|
},
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
label: _('Layout button sequence'),
|
|
screens: ['Main'],
|
|
submenu: layoutButtonSequenceOptions,
|
|
}, {
|
|
label: _('Toggle note list'),
|
|
screens: ['Main'],
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'toggleNoteList',
|
|
});
|
|
},
|
|
}, {
|
|
label: _('Toggle editor layout'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+L',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'toggleVisiblePanes',
|
|
});
|
|
},
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
label: Setting.settingMetadata('notes.sortOrder.field').label(),
|
|
screens: ['Main'],
|
|
submenu: sortNoteItems,
|
|
}, {
|
|
label: Setting.settingMetadata('folders.sortOrder.field').label(),
|
|
screens: ['Main'],
|
|
submenu: sortFolderItems,
|
|
}, {
|
|
label: Setting.settingMetadata('showNoteCounts').label(),
|
|
type: 'checkbox',
|
|
checked: Setting.value('showNoteCounts'),
|
|
screens: ['Main'],
|
|
click: () => {
|
|
Setting.setValue('showNoteCounts', !Setting.value('showNoteCounts'));
|
|
},
|
|
}, {
|
|
label: Setting.settingMetadata('uncompletedTodosOnTop').label(),
|
|
type: 'checkbox',
|
|
checked: Setting.value('uncompletedTodosOnTop'),
|
|
screens: ['Main'],
|
|
click: () => {
|
|
Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop'));
|
|
},
|
|
}, {
|
|
label: Setting.settingMetadata('showCompletedTodos').label(),
|
|
type: 'checkbox',
|
|
checked: Setting.value('showCompletedTodos'),
|
|
screens: ['Main'],
|
|
click: () => {
|
|
Setting.setValue('showCompletedTodos', !Setting.value('showCompletedTodos'));
|
|
},
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
label: _('Focus'),
|
|
screens: ['Main'],
|
|
submenu: focusItems,
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
label: _('Actual Size'),
|
|
click: () => {
|
|
Setting.setValue('windowContentZoomFactor', 100);
|
|
},
|
|
accelerator: 'CommandOrControl+0',
|
|
}, {
|
|
// There are 2 shortcuts for the action 'zoom in', mainly to increase the user experience.
|
|
// Most applications handle this the same way. These applications indicate Ctrl +, but actually mean Ctrl =.
|
|
// In fact they allow both: + and =. On the English keyboard layout - and = are used without the shift key.
|
|
// So to use Ctrl + would mean to use the shift key, but this is not the case in any of the apps that show Ctrl +.
|
|
// Additionally it allows the use of the plus key on the numpad.
|
|
label: _('Zoom In'),
|
|
click: () => {
|
|
Setting.incValue('windowContentZoomFactor', 10);
|
|
},
|
|
accelerator: 'CommandOrControl+Plus',
|
|
}, {
|
|
label: _('Zoom In'),
|
|
visible: false,
|
|
click: () => {
|
|
Setting.incValue('windowContentZoomFactor', 10);
|
|
},
|
|
accelerator: 'CommandOrControl+=',
|
|
}, {
|
|
label: _('Zoom Out'),
|
|
click: () => {
|
|
Setting.incValue('windowContentZoomFactor', -10);
|
|
},
|
|
accelerator: 'CommandOrControl+-',
|
|
}],
|
|
},
|
|
note: {
|
|
label: _('&Note'),
|
|
submenu: [{
|
|
id: 'edit:commandStartExternalEditing',
|
|
label: _('Edit in external editor'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+E',
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'commandStartExternalEditing',
|
|
});
|
|
},
|
|
}, {
|
|
id: 'edit:setTags',
|
|
label: _('Tags'),
|
|
screens: ['Main'],
|
|
accelerator: 'CommandOrControl+Alt+T',
|
|
click: () => {
|
|
const selectedNoteIds = this.store().getState().selectedNoteIds;
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'setTags',
|
|
noteIds: selectedNoteIds,
|
|
});
|
|
},
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
label: _('Statistics...'),
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'WINDOW_COMMAND',
|
|
name: 'commandContentProperties',
|
|
// text: this.state.note.body,
|
|
});
|
|
},
|
|
}],
|
|
},
|
|
tools: {
|
|
label: _('&Tools'),
|
|
submenu: toolsItems,
|
|
},
|
|
help: {
|
|
label: _('&Help'),
|
|
submenu: [{
|
|
label: _('Website and documentation'),
|
|
accelerator: 'F1',
|
|
click() { bridge().openExternal('https://joplinapp.org'); },
|
|
}, {
|
|
label: _('Joplin Forum'),
|
|
click() { bridge().openExternal('https://discourse.joplinapp.org'); },
|
|
}, {
|
|
label: _('Make a donation'),
|
|
click() { bridge().openExternal('https://joplinapp.org/donate/'); },
|
|
}, {
|
|
label: _('Check for updates...'),
|
|
visible: shim.isMac() ? false : true,
|
|
click: () => _checkForUpdates(this),
|
|
}, {
|
|
type: 'separator',
|
|
screens: ['Main'],
|
|
}, {
|
|
id: 'help:toggleDevTools',
|
|
type: 'checkbox',
|
|
label: _('Toggle development tools'),
|
|
visible: true,
|
|
click: () => {
|
|
this.dispatch({
|
|
type: 'NOTE_DEVTOOLS_TOGGLE',
|
|
});
|
|
},
|
|
}, {
|
|
type: 'separator',
|
|
visible: shim.isMac() ? false : true,
|
|
screens: ['Main'],
|
|
}, {
|
|
label: _('About Joplin'),
|
|
visible: shim.isMac() ? false : true,
|
|
click: () => _showAbout(),
|
|
}],
|
|
},
|
|
};
|
|
|
|
if (shim.isMac()) {
|
|
rootMenus.macOsApp = rootMenuFile;
|
|
rootMenus.file = rootMenuFileMacOs;
|
|
} else {
|
|
rootMenus.file = rootMenuFile;
|
|
}
|
|
|
|
// It seems the "visible" property of separators is ignored by Electron, making
|
|
// it display separators that we want hidden. So this function iterates through
|
|
// them and remove them completely.
|
|
const cleanUpSeparators = items => {
|
|
const output = [];
|
|
for (const item of items) {
|
|
if ('visible' in item && item.type === 'separator' && !item.visible) continue;
|
|
output.push(item);
|
|
}
|
|
return output;
|
|
};
|
|
|
|
for (const key in rootMenus) {
|
|
if (!rootMenus.hasOwnProperty(key)) continue;
|
|
if (!rootMenus[key].submenu) continue;
|
|
rootMenus[key].submenu = cleanUpSeparators(rootMenus[key].submenu);
|
|
}
|
|
|
|
const pluginMenuItems = PluginManager.instance().menuItems();
|
|
for (const item of pluginMenuItems) {
|
|
const itemParent = rootMenus[item.parent] ? rootMenus[item.parent] : 'tools';
|
|
itemParent.submenu.push(item);
|
|
}
|
|
|
|
const template = [
|
|
rootMenus.file,
|
|
rootMenus.edit,
|
|
rootMenus.view,
|
|
rootMenus.note,
|
|
rootMenus.tools,
|
|
rootMenus.help,
|
|
];
|
|
|
|
if (shim.isMac()) template.splice(0, 0, rootMenus.macOsApp);
|
|
|
|
function isEmptyMenu(template) {
|
|
for (let i = 0; i < template.length; i++) {
|
|
const t = template[i];
|
|
if (t.type !== 'separator') return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function removeUnwantedItems(template, screen) {
|
|
const platform = shim.platformName();
|
|
|
|
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.platforms && t.platforms.indexOf(platform) < 0) continue;
|
|
if (t.submenu) t.submenu = removeUnwantedItems(t.submenu, screen);
|
|
if (('submenu' in t) && isEmptyMenu(t.submenu)) continue;
|
|
output.push(t);
|
|
}
|
|
|
|
// Remove empty separator for now empty sections
|
|
const temp = [];
|
|
let previous = null;
|
|
for (let i = 0; i < output.length; i++) {
|
|
const t = Object.assign({}, output[i]);
|
|
if (t.type === 'separator') {
|
|
if (!previous) continue;
|
|
if (previous.type === 'separator') continue;
|
|
}
|
|
temp.push(t);
|
|
previous = t;
|
|
}
|
|
output = temp;
|
|
|
|
return output;
|
|
}
|
|
|
|
const screenTemplate = removeUnwantedItems(template, screen);
|
|
|
|
const menu = Menu.buildFromTemplate(screenTemplate);
|
|
Menu.setApplicationMenu(menu);
|
|
|
|
this.lastMenuScreen_ = screen;
|
|
}
|
|
|
|
async updateMenuItemStates(state = null) {
|
|
if (!this.lastMenuScreen_) return;
|
|
if (!this.store() && !state) return;
|
|
|
|
if (!state) state = this.store().getState();
|
|
|
|
const selectedNoteIds = state.selectedNoteIds;
|
|
const note = selectedNoteIds.length === 1 ? await Note.load(selectedNoteIds[0]) : null;
|
|
|
|
for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'showLocalSearch']) {
|
|
const menuItem = Menu.getApplicationMenu().getMenuItemById(`edit:${itemId}`);
|
|
if (!menuItem) continue;
|
|
menuItem.enabled = !!note && note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
|
}
|
|
|
|
const menuItem = Menu.getApplicationMenu().getMenuItemById('help:toggleDevTools');
|
|
menuItem.checked = state.devToolsVisible;
|
|
}
|
|
|
|
updateTray() {
|
|
const app = bridge().electronApp();
|
|
|
|
if (app.trayShown() === Setting.value('showTrayIcon')) return;
|
|
|
|
if (!Setting.value('showTrayIcon')) {
|
|
app.destroyTray();
|
|
} else {
|
|
const contextMenu = Menu.buildFromTemplate([
|
|
{ label: _('Open %s', app.electronApp().name), click: () => { app.window().show(); } },
|
|
{ type: 'separator' },
|
|
{ label: _('Exit'), click: () => { app.quit(); } },
|
|
]);
|
|
app.createTray(contextMenu);
|
|
}
|
|
}
|
|
|
|
updateEditorFont() {
|
|
const fontFamilies = [];
|
|
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
|
|
fontFamilies.push('monospace');
|
|
|
|
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
|
// https://github.com/laurent22/joplin/issues/155
|
|
|
|
const css = `.ace_editor * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
|
const styleTag = document.createElement('style');
|
|
styleTag.type = 'text/css';
|
|
styleTag.appendChild(document.createTextNode(css));
|
|
document.head.appendChild(styleTag);
|
|
}
|
|
|
|
async loadCustomCss(filePath) {
|
|
let cssString = '';
|
|
if (await fs.pathExists(filePath)) {
|
|
try {
|
|
cssString = await fs.readFile(filePath, 'utf-8');
|
|
|
|
} catch (error) {
|
|
let msg = error.message ? error.message : '';
|
|
msg = `Could not load custom css from ${filePath}\n${msg}`;
|
|
error.message = msg;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return cssString;
|
|
}
|
|
|
|
// async createManyNotes() {
|
|
// return;
|
|
// const folderIds = [];
|
|
|
|
// const randomFolderId = (folderIds) => {
|
|
// if (!folderIds.length) return '';
|
|
// const idx = Math.floor(Math.random() * folderIds.length);
|
|
// if (idx > folderIds.length - 1) throw new Error('Invalid index ' + idx + ' / ' + folderIds.length);
|
|
// return folderIds[idx];
|
|
// }
|
|
|
|
// let rootFolderCount = 0;
|
|
// let folderCount = 100;
|
|
|
|
// for (let i = 0; i < folderCount; i++) {
|
|
// let parentId = '';
|
|
|
|
// if (Math.random() >= 0.9 || rootFolderCount >= folderCount / 10) {
|
|
// parentId = randomFolderId(folderIds);
|
|
// } else {
|
|
// rootFolderCount++;
|
|
// }
|
|
|
|
// const folder = await Folder.save({ title: 'folder' + i, parent_id: parentId });
|
|
// folderIds.push(folder.id);
|
|
// }
|
|
|
|
// for (let i = 0; i < 10000; i++) {
|
|
// const parentId = randomFolderId(folderIds);
|
|
// Note.save({ title: 'note' + i, parent_id: parentId });
|
|
// }
|
|
// }
|
|
|
|
async start(argv) {
|
|
const electronIsDev = require('electron-is-dev');
|
|
|
|
// If running inside a package, the command line, instead of being "node.exe <path> <flags>" is "joplin.exe <flags>" so
|
|
// insert an extra argument so that they can be processed in a consistent way everywhere.
|
|
if (!electronIsDev) argv.splice(1, 0, '.');
|
|
|
|
argv = await super.start(argv);
|
|
|
|
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
|
|
const dir = Setting.value('profileDir');
|
|
const filename = Setting.custom_css_files.JOPLIN_APP;
|
|
await CssUtils.injectCustomStyles(`${dir}/${filename}`);
|
|
|
|
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
|
AlarmService.setLogger(reg.logger());
|
|
|
|
reg.setShowErrorMessageBoxHandler((message) => { bridge().showErrorMessageBox(message); });
|
|
|
|
if (Setting.value('flagOpenDevTools')) {
|
|
bridge().openDevTools();
|
|
}
|
|
|
|
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
|
PluginManager.instance().setLogger(reg.logger());
|
|
PluginManager.instance().register(pluginClasses);
|
|
|
|
this.updateMenu('Main');
|
|
|
|
this.initRedux();
|
|
|
|
// Since the settings need to be loaded before the store is created, it will never
|
|
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
|
|
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
|
Setting.dispatchUpdateAll();
|
|
|
|
await FoldersScreenUtils.refreshFolders();
|
|
|
|
const tags = await Tag.allWithNotes();
|
|
|
|
this.dispatch({
|
|
type: 'TAG_UPDATE_ALL',
|
|
items: tags,
|
|
});
|
|
|
|
const masterKeys = await MasterKey.all();
|
|
|
|
this.dispatch({
|
|
type: 'MASTERKEY_UPDATE_ALL',
|
|
items: masterKeys,
|
|
});
|
|
|
|
this.store().dispatch({
|
|
type: 'FOLDER_SELECT',
|
|
id: Setting.value('activeFolderId'),
|
|
});
|
|
|
|
this.store().dispatch({
|
|
type: 'FOLDER_SET_COLLAPSED_ALL',
|
|
ids: Setting.value('collapsedFolderIds'),
|
|
});
|
|
|
|
// Loads custom Markdown preview styles
|
|
const cssString = await CssUtils.loadCustomCss(`${Setting.value('profileDir')}/userstyle.css`);
|
|
this.store().dispatch({
|
|
type: 'LOAD_CUSTOM_CSS',
|
|
css: cssString,
|
|
});
|
|
|
|
const templates = await TemplateUtils.loadTemplates(Setting.value('templateDir'));
|
|
|
|
this.store().dispatch({
|
|
type: 'TEMPLATE_UPDATE_ALL',
|
|
templates: templates,
|
|
});
|
|
|
|
this.store().dispatch({
|
|
type: 'NOTE_DEVTOOLS_SET',
|
|
value: Setting.value('flagOpenDevTools'),
|
|
});
|
|
|
|
// 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 = () => {
|
|
if (Setting.value('autoUpdateEnabled')) {
|
|
bridge().checkForUpdates(true, bridge().window(), this.checkForUpdateLoggerPath(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
|
}
|
|
};
|
|
|
|
// Initial check on startup
|
|
setTimeout(() => { runAutoUpdateCheck(); }, 5000);
|
|
// Then every x hours
|
|
setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
|
|
}
|
|
|
|
this.updateTray();
|
|
|
|
setTimeout(() => {
|
|
AlarmService.garbageCollect();
|
|
}, 1000 * 60 * 60);
|
|
|
|
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
|
|
// Keep it hidden
|
|
} else {
|
|
bridge().window().show();
|
|
}
|
|
|
|
ResourceService.runInBackground();
|
|
|
|
if (Setting.value('env') === 'dev') {
|
|
AlarmService.updateAllNotifications();
|
|
} else {
|
|
reg.scheduleSync().then(() => {
|
|
// Wait for the first sync before updating the notifications, since synchronisation
|
|
// might change the notifications.
|
|
AlarmService.updateAllNotifications();
|
|
|
|
DecryptionWorker.instance().scheduleStart();
|
|
});
|
|
}
|
|
|
|
const clipperLogger = new Logger();
|
|
clipperLogger.addTarget('file', { path: `${Setting.value('profileDir')}/log-clipper.txt` });
|
|
clipperLogger.addTarget('console');
|
|
|
|
ClipperServer.instance().setLogger(clipperLogger);
|
|
ClipperServer.instance().setDispatch(this.store().dispatch);
|
|
|
|
if (Setting.value('clipperServer.autoStart')) {
|
|
ClipperServer.instance().start();
|
|
}
|
|
|
|
ExternalEditWatcher.instance().setLogger(reg.logger());
|
|
ExternalEditWatcher.instance().dispatch = this.store().dispatch;
|
|
|
|
RevisionService.instance().runInBackground();
|
|
|
|
this.updateMenuItemStates();
|
|
|
|
// Make it available to the console window - useful to call revisionService.collectRevisions()
|
|
window.revisionService = RevisionService.instance();
|
|
window.migrationService = MigrationService.instance();
|
|
window.decryptionWorker = DecryptionWorker.instance();
|
|
}
|
|
|
|
}
|
|
|
|
let application_ = null;
|
|
|
|
function app() {
|
|
if (!application_) application_ = new Application();
|
|
return application_;
|
|
}
|
|
|
|
module.exports = { app };
|