2020-11-07 17:59:37 +02:00
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index' ;
import CommandService from '@joplin/lib/services/CommandService' ;
import KeymapService from '@joplin/lib/services/KeymapService' ;
2021-08-05 13:48:39 +02:00
import PluginService , { PluginSettings } from '@joplin/lib/services/plugins/PluginService' ;
2020-11-07 17:59:37 +02:00
import resourceEditWatcherReducer , { defaultState as resourceEditWatcherDefaultState } from '@joplin/lib/services/ResourceEditWatcher/reducer' ;
import { defaultState , State } from '@joplin/lib/reducer' ;
2020-10-09 19:35:46 +02:00
import PluginRunner from './services/plugins/PluginRunner' ;
import PlatformImplementation from './services/plugins/PlatformImplementation' ;
2020-11-07 17:59:37 +02:00
import shim from '@joplin/lib/shim' ;
import AlarmService from '@joplin/lib/services/AlarmService' ;
import AlarmServiceDriverNode from '@joplin/lib/services/AlarmServiceDriverNode' ;
import Logger , { TargetType } from '@joplin/lib/Logger' ;
import Setting from '@joplin/lib/models/Setting' ;
import actionApi from '@joplin/lib/services/rest/actionApi.desktop' ;
import BaseApplication from '@joplin/lib/BaseApplication' ;
2021-05-13 18:57:37 +02:00
import DebugService from '@joplin/lib/debug/DebugService' ;
2020-11-07 17:59:37 +02:00
import { _ , setLocale } from '@joplin/lib/locale' ;
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService' ;
2020-11-05 18:58:23 +02:00
import SpellCheckerServiceDriverNative from './services/spellChecker/SpellCheckerServiceDriverNative' ;
import bridge from './services/bridge' ;
2020-10-31 18:29:17 +02:00
import menuCommandNames from './gui/menuCommandNames' ;
2020-11-13 19:09:28 +02:00
import { LayoutItem } from './gui/ResizableLayout/utils/types' ;
import stateToWhenClauseContext from './services/commands/stateToWhenClauseContext' ;
import ResourceService from '@joplin/lib/services/ResourceService' ;
2020-11-16 13:03:44 +02:00
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher' ;
2021-01-02 15:32:15 +02:00
import produce from 'immer' ;
import iterateItems from './gui/ResizableLayout/utils/iterateItems' ;
import validateLayout from './gui/ResizableLayout/utils/validateLayout' ;
2020-11-07 17:59:37 +02:00
const { FoldersScreenUtils } = require ( '@joplin/lib/folders-screen-utils.js' ) ;
2021-01-22 19:41:11 +02:00
import Folder from '@joplin/lib/models/Folder' ;
2020-10-09 19:35:46 +02:00
const fs = require ( 'fs-extra' ) ;
2021-01-22 19:41:11 +02:00
import Tag from '@joplin/lib/models/Tag' ;
2021-01-29 20:45:11 +02:00
import { reg } from '@joplin/lib/registry' ;
2020-10-09 19:35:46 +02:00
const packageInfo = require ( './packageInfo.js' ) ;
2021-01-23 17:51:19 +02:00
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker' ;
2021-06-22 20:57:04 +02:00
import ClipperServer from '@joplin/lib/ClipperServer' ;
2020-10-09 19:35:46 +02:00
const { webFrame } = require ( 'electron' ) ;
const Menu = bridge ( ) . Menu ;
2020-11-07 17:59:37 +02:00
const PluginManager = require ( '@joplin/lib/services/PluginManager' ) ;
2021-01-23 17:51:19 +02:00
import RevisionService from '@joplin/lib/services/RevisionService' ;
import MigrationService from '@joplin/lib/services/MigrationService' ;
2021-07-23 12:05:21 +02:00
import { loadCustomCss , injectCustomStyles } from '@joplin/lib/CssUtils' ;
2021-01-22 19:41:11 +02:00
// import populateDatabase from '@joplin/lib/services/debug/populateDatabase';
2020-10-09 19:35:46 +02:00
const commands = [
require ( './gui/MainScreen/commands/editAlarm' ) ,
require ( './gui/MainScreen/commands/exportPdf' ) ,
2021-06-27 15:14:11 +02:00
require ( './gui/MainScreen/commands/gotoAnything' ) ,
2021-08-18 12:54:28 +02:00
require ( './gui/MainScreen/commands/commandPalette' ) ,
2020-10-09 19:35:46 +02:00
require ( './gui/MainScreen/commands/hideModalMessage' ) ,
require ( './gui/MainScreen/commands/moveToFolder' ) ,
require ( './gui/MainScreen/commands/newFolder' ) ,
2020-11-17 16:50:28 +02:00
require ( './gui/MainScreen/commands/newNote' ) ,
2020-10-25 19:29:52 +02:00
require ( './gui/MainScreen/commands/newSubFolder' ) ,
2020-10-09 19:35:46 +02:00
require ( './gui/MainScreen/commands/newTodo' ) ,
2020-11-17 16:50:28 +02:00
require ( './gui/MainScreen/commands/openFolder' ) ,
require ( './gui/MainScreen/commands/openNote' ) ,
require ( './gui/MainScreen/commands/openTag' ) ,
2020-10-09 19:35:46 +02:00
require ( './gui/MainScreen/commands/print' ) ,
require ( './gui/MainScreen/commands/renameFolder' ) ,
require ( './gui/MainScreen/commands/renameTag' ) ,
require ( './gui/MainScreen/commands/search' ) ,
require ( './gui/MainScreen/commands/setTags' ) ,
require ( './gui/MainScreen/commands/showModalMessage' ) ,
require ( './gui/MainScreen/commands/showNoteContentProperties' ) ,
require ( './gui/MainScreen/commands/showNoteProperties' ) ,
2021-06-27 15:14:11 +02:00
require ( './gui/MainScreen/commands/showPrompt' ) ,
require ( './gui/MainScreen/commands/showShareFolderDialog' ) ,
2020-10-09 19:35:46 +02:00
require ( './gui/MainScreen/commands/showShareNoteDialog' ) ,
2020-11-08 03:08:33 +02:00
require ( './gui/MainScreen/commands/showSpellCheckerMenu' ) ,
2020-11-17 16:50:28 +02:00
require ( './gui/MainScreen/commands/toggleEditors' ) ,
require ( './gui/MainScreen/commands/toggleLayoutMoveMode' ) ,
2020-10-09 19:35:46 +02:00
require ( './gui/MainScreen/commands/toggleNoteList' ) ,
2020-10-18 22:52:10 +02:00
require ( './gui/MainScreen/commands/toggleSideBar' ) ,
2020-10-09 19:35:46 +02:00
require ( './gui/MainScreen/commands/toggleVisiblePanes' ) ,
require ( './gui/NoteEditor/commands/focusElementNoteBody' ) ,
require ( './gui/NoteEditor/commands/focusElementNoteTitle' ) ,
require ( './gui/NoteEditor/commands/showLocalSearch' ) ,
require ( './gui/NoteEditor/commands/showRevisions' ) ,
require ( './gui/NoteList/commands/focusElementNoteList' ) ,
2020-11-17 16:50:28 +02:00
require ( './gui/NoteListControls/commands/focusSearch' ) ,
2021-01-17 13:34:37 +02:00
require ( './gui/Sidebar/commands/focusElementSideBar' ) ,
2020-10-09 19:35:46 +02:00
] ;
// Commands that are not tied to any particular component.
// The runtime for these commands can be loaded when the app starts.
const globalCommands = [
2020-11-17 16:50:28 +02:00
require ( './commands/copyDevCommand' ) ,
require ( './commands/exportFolders' ) ,
require ( './commands/exportNotes' ) ,
2020-10-09 19:35:46 +02:00
require ( './commands/focusElement' ) ,
2020-11-17 16:50:28 +02:00
require ( './commands/openProfileDirectory' ) ,
2021-04-24 20:23:33 +02:00
require ( './commands/replaceMisspelling' ) ,
2020-10-09 19:35:46 +02:00
require ( './commands/startExternalEditing' ) ,
require ( './commands/stopExternalEditing' ) ,
2020-10-10 14:32:30 +02:00
require ( './commands/toggleExternalEditing' ) ,
2021-04-24 20:23:33 +02:00
require ( './commands/toggleSafeMode' ) ,
2021-06-10 11:49:20 +02:00
require ( './commands/restoreNoteRevision' ) ,
2020-11-07 17:59:37 +02:00
require ( '@joplin/lib/commands/historyBackward' ) ,
require ( '@joplin/lib/commands/historyForward' ) ,
2020-11-17 16:50:28 +02:00
require ( '@joplin/lib/commands/synchronize' ) ,
2020-10-09 19:35:46 +02:00
] ;
2021-01-22 19:41:11 +02:00
import editorCommandDeclarations from './gui/NoteEditor/commands/editorCommandDeclarations' ;
2021-05-13 18:57:37 +02:00
import ShareService from '@joplin/lib/services/share/ShareService' ;
2021-05-14 11:29:06 +02:00
import checkForUpdates from './checkForUpdates' ;
2020-10-09 19:35:46 +02:00
const pluginClasses = [
2020-10-18 22:52:10 +02:00
require ( './plugins/GotoAnything' ) . default ,
2020-10-09 19:35:46 +02:00
] ;
interface AppStateRoute {
2020-11-12 21:29:22 +02:00
type : string ;
routeName : string ;
props : any ;
2020-10-09 19:35:46 +02:00
}
2021-08-16 16:20:14 +02:00
export interface AppStateDialog {
name : string ;
}
2020-10-09 19:35:46 +02:00
export interface AppState extends State {
2020-11-12 21:29:22 +02:00
route : AppStateRoute ;
navHistory : any [ ] ;
noteVisiblePanes : string [ ] ;
windowContentSize : any ;
watchedNoteFiles : string [ ] ;
lastEditorScrollPercents : any ;
devToolsVisible : boolean ;
visibleDialogs : any ; // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
focusedField : string ;
2020-11-13 19:09:28 +02:00
layoutMoveMode : boolean ;
2021-02-07 18:47:56 +02:00
startupPluginsLoaded : boolean ;
2020-10-09 19:35:46 +02:00
// Extra reducer keys go here
2020-11-12 21:29:22 +02:00
watchedResources : any ;
2020-11-13 19:09:28 +02:00
mainLayout : LayoutItem ;
2021-08-16 16:20:14 +02:00
dialogs : AppStateDialog [ ] ;
2020-10-09 19:35:46 +02:00
}
2020-11-12 21:13:28 +02:00
const appDefaultState : AppState = {
2020-10-09 19:35:46 +02:00
. . . defaultState ,
route : {
type : 'NAV_GO' ,
routeName : 'Main' ,
props : { } ,
} ,
navHistory : [ ] ,
noteVisiblePanes : [ 'editor' , 'viewer' ] ,
windowContentSize : bridge ( ) . windowContentSize ( ) ,
watchedNoteFiles : [ ] ,
lastEditorScrollPercents : { } ,
devToolsVisible : false ,
visibleDialogs : { } , // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
focusedField : null ,
2020-11-13 19:09:28 +02:00
layoutMoveMode : false ,
mainLayout : null ,
2021-02-07 18:47:56 +02:00
startupPluginsLoaded : false ,
2021-08-16 16:20:14 +02:00
dialogs : [ ] ,
2020-10-09 19:35:46 +02:00
. . . resourceEditWatcherDefaultState ,
} ;
class Application extends BaseApplication {
2021-02-07 18:47:56 +02:00
private checkAllPluginStartedIID_ : any = null ;
2020-10-09 19:35:46 +02:00
constructor ( ) {
super ( ) ;
this . bridge_nativeThemeUpdated = this . bridge_nativeThemeUpdated . bind ( this ) ;
}
hasGui() {
return true ;
}
2020-11-12 21:13:28 +02:00
reducer ( state : AppState = appDefaultState , action : any ) {
2020-10-09 19:35:46 +02:00
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 ;
2021-02-07 18:47:56 +02:00
case 'STARTUP_PLUGINS_LOADED' :
// When all startup plugins have loaded, we also recreate the
// main layout to ensure that it is updated in the UI. There's
// probably a cleaner way to do this, but for now that will do.
if ( state . startupPluginsLoaded !== action . value ) {
newState = {
. . . newState ,
startupPluginsLoaded : action.value ,
mainLayout : JSON.parse ( JSON . stringify ( newState . mainLayout ) ) ,
} ;
}
break ;
2020-10-09 19:35:46 +02:00
case 'WINDOW_CONTENT_SIZE_SET' :
newState = Object . assign ( { } , state ) ;
newState . windowContentSize = action . size ;
break ;
case 'NOTE_VISIBLE_PANES_TOGGLE' :
{
2020-11-12 21:13:28 +02:00
const getNextLayout = ( currentLayout : any ) = > {
2020-10-09 19:35:46 +02:00
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 ;
2020-11-13 19:09:28 +02:00
case 'MAIN_LAYOUT_SET' :
2020-10-09 19:35:46 +02:00
2020-11-13 19:09:28 +02:00
newState = {
. . . state ,
mainLayout : action.value ,
} ;
2020-10-09 19:35:46 +02:00
break ;
2021-01-02 15:32:15 +02:00
case 'MAIN_LAYOUT_SET_ITEM_PROP' :
{
let newLayout = produce ( state . mainLayout , ( draftLayout : LayoutItem ) = > {
iterateItems ( draftLayout , ( _itemIndex : number , item : LayoutItem , _parent : LayoutItem ) = > {
if ( item . key === action . itemKey ) {
( item as any ) [ action . propName ] = action . propValue ;
return false ;
}
return true ;
} ) ;
} ) ;
if ( newLayout !== state . mainLayout ) newLayout = validateLayout ( newLayout ) ;
newState = {
. . . state ,
mainLayout : newLayout ,
} ;
}
break ;
2020-10-09 19:35:46 +02:00
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 ;
case 'VISIBLE_DIALOGS_ADD' :
newState = Object . assign ( { } , state ) ;
newState . visibleDialogs = Object . assign ( { } , newState . visibleDialogs ) ;
newState . visibleDialogs [ action . name ] = true ;
break ;
case 'VISIBLE_DIALOGS_REMOVE' :
newState = Object . assign ( { } , state ) ;
newState . visibleDialogs = Object . assign ( { } , newState . visibleDialogs ) ;
delete newState . visibleDialogs [ action . name ] ;
break ;
case 'FOCUS_SET' :
newState = Object . assign ( { } , state ) ;
newState . focusedField = action . field ;
break ;
case 'FOCUS_CLEAR' :
// A field can only clear its own state
if ( action . field === state . focusedField ) {
newState = Object . assign ( { } , state ) ;
newState . focusedField = null ;
}
break ;
2021-08-16 16:20:14 +02:00
case 'DIALOG_OPEN' :
{
newState = Object . assign ( { } , state ) ;
const newDialogs = newState . dialogs . slice ( ) ;
if ( newDialogs . find ( d = > d . name === action . name ) ) throw new Error ( ` This dialog is already opened: ${ action . name } ` ) ;
newDialogs . push ( {
name : action.name ,
} ) ;
newState . dialogs = newDialogs ;
}
break ;
case 'DIALOG_CLOSE' :
{
newState = Object . assign ( { } , state ) ;
const newDialogs = newState . dialogs . slice ( ) . filter ( d = > d . name !== action . name ) ;
newState . dialogs = newDialogs ;
}
break ;
2020-11-13 19:09:28 +02:00
case 'LAYOUT_MOVE_MODE_SET' :
newState = {
. . . state ,
layoutMoveMode : action.value ,
} ;
break ;
2020-10-09 19:35:46 +02:00
}
} catch ( error ) {
error . message = ` In reducer: ${ error . message } Action: ${ JSON . stringify ( action ) } ` ;
throw error ;
}
newState = resourceEditWatcherReducer ( newState , action ) ;
newState = super . reducer ( newState , action ) ;
return newState ;
}
2020-11-12 21:13:28 +02:00
toggleDevTools ( visible : boolean ) {
2020-10-09 19:35:46 +02:00
if ( visible ) {
bridge ( ) . openDevTools ( ) ;
} else {
bridge ( ) . closeDevTools ( ) ;
}
}
2020-11-12 21:13:28 +02:00
async generalMiddleware ( store : any , next : any , action : any ) {
2020-10-09 19:35:46 +02:00
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' ) ) ;
}
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 ( [ 'NOTE_VISIBLE_PANES_TOGGLE' , 'NOTE_VISIBLE_PANES_SET' ] . indexOf ( action . type ) >= 0 ) {
Setting . setValue ( 'noteVisiblePanes' , newState . noteVisiblePanes ) ;
}
if ( [ 'NOTE_DEVTOOLS_TOGGLE' , 'NOTE_DEVTOOLS_SET' ] . indexOf ( action . type ) >= 0 ) {
this . toggleDevTools ( newState . devToolsVisible ) ;
}
if ( action . type === 'FOLDER_AND_NOTE_SELECT' ) {
await Folder . expandTree ( newState . folders , action . folderId ) ;
}
if ( this . hasGui ( ) && ( ( action . type == 'SETTING_UPDATE_ONE' && [ 'themeAutoDetect' , 'theme' , 'preferredLightTheme' , 'preferredDarkTheme' ] . includes ( action . key ) ) || action . type == 'SETTING_UPDATE_ALL' ) ) {
this . handleThemeAutoDetect ( ) ;
}
return result ;
}
handleThemeAutoDetect() {
if ( ! Setting . value ( 'themeAutoDetect' ) ) return ;
if ( bridge ( ) . shouldUseDarkColors ( ) ) {
Setting . setValue ( 'theme' , Setting . value ( 'preferredDarkTheme' ) ) ;
} else {
Setting . setValue ( 'theme' , Setting . value ( 'preferredLightTheme' ) ) ;
}
}
bridge_nativeThemeUpdated() {
this . handleThemeAutoDetect ( ) ;
}
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' } ,
2020-11-25 16:40:25 +02:00
{ label : _ ( 'Quit' ) , click : ( ) = > { void app . quit ( ) ; } } ,
2020-10-09 19:35:46 +02:00
] ) ;
app . createTray ( contextMenu ) ;
}
}
updateEditorFont() {
const fontFamilies = [ ] ;
if ( Setting . value ( 'style.editor.fontFamily' ) ) fontFamilies . push ( ` " ${ Setting . value ( 'style.editor.fontFamily' ) } " ` ) ;
2021-03-26 11:08:22 +02:00
fontFamilies . push ( 'Avenir, Arial, sans-serif' ) ;
2020-10-09 19:35:46 +02:00
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
// https://github.com/laurent22/joplin/issues/155
const css = ` .CodeMirror * { font-family: ${ fontFamilies . join ( ', ' ) } !important; } ` ;
const styleTag = document . createElement ( 'style' ) ;
styleTag . type = 'text/css' ;
styleTag . appendChild ( document . createTextNode ( css ) ) ;
document . head . appendChild ( styleTag ) ;
}
2020-11-05 18:58:23 +02:00
setupContextMenu() {
2020-11-08 03:08:33 +02:00
const MenuItem = bridge ( ) . MenuItem ;
2020-11-05 18:58:23 +02:00
// The context menu must be setup in renderer process because that's where
// the spell checker service lives.
require ( 'electron-context-menu' ) ( {
2020-11-12 21:13:28 +02:00
shouldShowMenu : ( _event : any , params : any ) = > {
2020-11-05 18:58:23 +02:00
// params.inputFieldType === 'none' when right-clicking the text editor. This is a bit of a hack to detect it because in this
// case we don't want to use the built-in context menu but a custom one.
return params . isEditable && params . inputFieldType !== 'none' ;
} ,
2020-11-12 21:13:28 +02:00
menu : ( actions : any , props : any ) = > {
const spellCheckerMenuItems = SpellCheckerService . instance ( ) . contextMenuItems ( props . misspelledWord , props . dictionarySuggestions ) . map ( ( item : any ) = > new MenuItem ( item ) ) ;
2020-11-05 18:58:23 +02:00
const output = [
actions . cut ( ) ,
actions . copy ( ) ,
actions . paste ( ) ,
. . . spellCheckerMenuItems ,
] ;
return output ;
} ,
} ) ;
}
2020-11-12 21:13:28 +02:00
async loadCustomCss ( filePath : string ) {
2020-10-09 19:35:46 +02:00
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 ;
}
2021-08-06 11:58:32 +02:00
private async checkForLegacyTemplates() {
const templatesDir = ` ${ Setting . value ( 'profileDir' ) } /templates ` ;
if ( await shim . fsDriver ( ) . exists ( templatesDir ) ) {
try {
const files = await shim . fsDriver ( ) . readDirStats ( templatesDir ) ;
for ( const file of files ) {
if ( file . path . endsWith ( '.md' ) ) {
// There is atleast one template.
this . store ( ) . dispatch ( {
type : 'CONTAINS_LEGACY_TEMPLATES' ,
} ) ;
break ;
}
}
} catch ( error ) {
reg . logger ( ) . error ( ` Failed to read templates directory: ${ error } ` ) ;
}
}
}
2020-11-13 19:09:28 +02:00
private async initPluginService() {
2020-11-19 14:34:49 +02:00
const service = PluginService . instance ( ) ;
2020-11-13 19:09:28 +02:00
const pluginRunner = new PluginRunner ( ) ;
2020-11-19 14:34:49 +02:00
service . initialize ( packageInfo . version , PlatformImplementation . instance ( ) , pluginRunner , this . store ( ) ) ;
2021-04-24 20:23:33 +02:00
service . isSafeMode = Setting . value ( 'isSafeMode' ) ;
2020-11-19 14:34:49 +02:00
const pluginSettings = service . unserializePluginSettings ( Setting . value ( 'plugins.states' ) ) ;
2021-08-05 13:48:39 +02:00
{
// Users can add and remove plugins from the config screen at any
// time, however we only effectively uninstall the plugin the next
// time the app is started. What plugin should be uninstalled is
// stored in the settings.
const newSettings = service . clearUpdateState ( await service . uninstallPlugins ( pluginSettings ) ) ;
Setting . setValue ( 'plugins.states' , newSettings ) ;
}
2020-11-13 19:09:28 +02:00
try {
2020-11-19 14:34:49 +02:00
if ( await shim . fsDriver ( ) . exists ( Setting . value ( 'pluginDir' ) ) ) {
await service . loadAndRunPlugins ( Setting . value ( 'pluginDir' ) , pluginSettings ) ;
}
2020-11-13 19:09:28 +02:00
} catch ( error ) {
this . logger ( ) . error ( ` There was an error loading plugins from ${ Setting . value ( 'pluginDir' ) } : ` , error ) ;
}
try {
if ( Setting . value ( 'plugins.devPluginPaths' ) ) {
const paths = Setting . value ( 'plugins.devPluginPaths' ) . split ( ',' ) . map ( ( p : string ) = > p . trim ( ) ) ;
2020-11-19 14:34:49 +02:00
await service . loadAndRunPlugins ( paths , pluginSettings , true ) ;
2020-11-13 19:09:28 +02:00
}
// Also load dev plugins that have passed via command line arguments
if ( Setting . value ( 'startupDevPlugins' ) ) {
2020-11-19 14:34:49 +02:00
await service . loadAndRunPlugins ( Setting . value ( 'startupDevPlugins' ) , pluginSettings , true ) ;
2020-11-13 19:09:28 +02:00
}
} catch ( error ) {
this . logger ( ) . error ( ` There was an error loading plugins from ${ Setting . value ( 'plugins.devPluginPaths' ) } : ` , error ) ;
}
2021-02-07 18:47:56 +02:00
2021-08-05 13:48:39 +02:00
{
// Users can potentially delete files from /plugins or even delete
// the complete folder. When that happens, we still have the plugin
// info in the state, which can cause various issues, so to sort it
// out we remove from the state any plugin that has *not* been loaded
// above (meaning the file was missing).
// https://github.com/laurent22/joplin/issues/5253
const oldSettings = service . unserializePluginSettings ( Setting . value ( 'plugins.states' ) ) ;
const newSettings : PluginSettings = { } ;
for ( const pluginId of Object . keys ( oldSettings ) ) {
if ( ! service . pluginIds . includes ( pluginId ) ) {
this . logger ( ) . warn ( 'Found a plugin in the state that has not been loaded, which means the plugin might have been deleted outside Joplin - removing it from the state:' , pluginId ) ;
continue ;
}
newSettings [ pluginId ] = oldSettings [ pluginId ] ;
}
Setting . setValue ( 'plugins.states' , newSettings ) ;
}
2021-02-07 18:47:56 +02:00
this . checkAllPluginStartedIID_ = setInterval ( ( ) = > {
if ( service . allPluginsStarted ) {
clearInterval ( this . checkAllPluginStartedIID_ ) ;
this . dispatch ( {
type : 'STARTUP_PLUGINS_LOADED' ,
value : true ,
} ) ;
}
} , 500 ) ;
2020-11-13 19:09:28 +02:00
}
2020-11-12 21:13:28 +02:00
async start ( argv : string [ ] ) : Promise < any > {
2020-10-09 19:35:46 +02:00
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 ) ;
await this . applySettingsSideEffects ( ) ;
if ( Setting . value ( 'sync.upgradeState' ) === Setting . SYNC_UPGRADE_STATE_MUST_DO ) {
reg . logger ( ) . info ( 'app.start: doing upgradeSyncTarget action' ) ;
bridge ( ) . window ( ) . show ( ) ;
return { action : 'upgradeSyncTarget' } ;
}
reg . logger ( ) . info ( 'app.start: doing regular boot' ) ;
const dir = Setting . value ( 'profileDir' ) ;
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
const filename = Setting . custom_css_files . JOPLIN_APP ;
2021-07-23 12:05:21 +02:00
await injectCustomStyles ( 'appStyles' , ` ${ dir } / ${ filename } ` ) ;
2020-10-09 19:35:46 +02:00
AlarmService . setDriver ( new AlarmServiceDriverNode ( { appName : packageInfo.build.appId } ) ) ;
AlarmService . setLogger ( reg . logger ( ) ) ;
2020-11-12 21:13:28 +02:00
reg . setShowErrorMessageBoxHandler ( ( message : string ) = > { bridge ( ) . showErrorMessageBox ( message ) ; } ) ;
2020-10-09 19:35:46 +02:00
if ( Setting . value ( 'flagOpenDevTools' ) ) {
bridge ( ) . openDevTools ( ) ;
}
PluginManager . instance ( ) . dispatch_ = this . dispatch . bind ( this ) ;
PluginManager . instance ( ) . setLogger ( reg . logger ( ) ) ;
PluginManager . instance ( ) . register ( pluginClasses ) ;
this . initRedux ( ) ;
2020-11-13 19:09:28 +02:00
CommandService . instance ( ) . initialize ( this . store ( ) , Setting . value ( 'env' ) == 'dev' , stateToWhenClauseContext ) ;
2020-10-09 19:35:46 +02:00
for ( const command of commands ) {
CommandService . instance ( ) . registerDeclaration ( command . declaration ) ;
}
for ( const command of globalCommands ) {
CommandService . instance ( ) . registerDeclaration ( command . declaration ) ;
CommandService . instance ( ) . registerRuntime ( command . declaration . name , command . runtime ( ) ) ;
}
for ( const declaration of editorCommandDeclarations ) {
CommandService . instance ( ) . registerDeclaration ( declaration ) ;
}
2020-10-31 14:25:12 +02:00
const keymapService = KeymapService . instance ( ) ;
2020-10-31 18:29:17 +02:00
// We only add the commands that appear in the menu because only
// those can have a shortcut associated with them.
keymapService . initialize ( menuCommandNames ( ) ) ;
2020-10-31 14:25:12 +02:00
try {
await keymapService . loadCustomKeymap ( ` ${ dir } /keymap-desktop.json ` ) ;
} catch ( error ) {
reg . logger ( ) . error ( error ) ;
}
2020-10-31 18:29:17 +02:00
// 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.
2020-10-09 19:35:46 +02:00
Setting . dispatchUpdateAll ( ) ;
await FoldersScreenUtils . refreshFolders ( ) ;
const tags = await Tag . allWithNotes ( ) ;
this . dispatch ( {
type : 'TAG_UPDATE_ALL' ,
items : tags ,
} ) ;
2021-08-12 17:54:10 +02:00
// const masterKeys = await MasterKey.all();
2020-10-09 19:35:46 +02:00
2021-08-12 17:54:10 +02:00
// this.dispatch({
// type: 'MASTERKEY_UPDATE_ALL',
// items: masterKeys,
// });
2020-10-09 19:35:46 +02:00
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
2021-07-23 12:05:21 +02:00
const cssString = await loadCustomCss ( ` ${ Setting . value ( 'profileDir' ) } /userstyle.css ` ) ;
2020-10-09 19:35:46 +02:00
this . store ( ) . dispatch ( {
2021-07-23 12:05:21 +02:00
type : 'CUSTOM_CSS_APPEND' ,
2020-10-09 19:35:46 +02:00
css : cssString ,
} ) ;
this . store ( ) . dispatch ( {
type : 'NOTE_DEVTOOLS_SET' ,
value : Setting.value ( 'flagOpenDevTools' ) ,
} ) ;
2021-08-06 11:58:32 +02:00
await this . checkForLegacyTemplates ( ) ;
2020-10-09 19:35:46 +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 = ( ) = > {
if ( Setting . value ( 'autoUpdateEnabled' ) ) {
2021-05-14 11:29:06 +02:00
void checkForUpdates ( true , bridge ( ) . window ( ) , { includePreReleases : Setting.value ( 'autoUpdate.includePreReleases' ) } ) ;
2020-10-09 19:35:46 +02:00
}
} ;
// Initial check on startup
shim . setTimeout ( ( ) = > { runAutoUpdateCheck ( ) ; } , 5000 ) ;
// Then every x hours
shim . setInterval ( ( ) = > { runAutoUpdateCheck ( ) ; } , 12 * 60 * 60 * 1000 ) ;
}
this . updateTray ( ) ;
shim . setTimeout ( ( ) = > {
2020-11-25 16:40:25 +02:00
void AlarmService . garbageCollect ( ) ;
2020-10-09 19:35:46 +02:00
} , 1000 * 60 * 60 ) ;
if ( Setting . value ( 'startMinimized' ) && Setting . value ( 'showTrayIcon' ) ) {
// Keep it hidden
} else {
bridge ( ) . window ( ) . show ( ) ;
}
2021-05-13 18:57:37 +02:00
void ShareService . instance ( ) . maintenance ( ) ;
2020-10-09 19:35:46 +02:00
ResourceService . runInBackground ( ) ;
if ( Setting . value ( 'env' ) === 'dev' ) {
2020-11-25 16:40:25 +02:00
void AlarmService . updateAllNotifications ( ) ;
2020-10-09 19:35:46 +02:00
} else {
2021-01-29 20:45:11 +02:00
void reg . scheduleSync ( 1000 ) . then ( ( ) = > {
2020-10-09 19:35:46 +02:00
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
2020-11-25 16:40:25 +02:00
void AlarmService . updateAllNotifications ( ) ;
2020-10-09 19:35:46 +02:00
2021-01-22 19:41:11 +02:00
void DecryptionWorker . instance ( ) . scheduleStart ( ) ;
2020-10-09 19:35:46 +02:00
} ) ;
}
const clipperLogger = new Logger ( ) ;
clipperLogger . addTarget ( TargetType . File , { path : ` ${ Setting . value ( 'profileDir' ) } /log-clipper.txt ` } ) ;
clipperLogger . addTarget ( TargetType . Console ) ;
ClipperServer . instance ( ) . initialize ( actionApi ) ;
ClipperServer . instance ( ) . setLogger ( clipperLogger ) ;
ClipperServer . instance ( ) . setDispatch ( this . store ( ) . dispatch ) ;
if ( Setting . value ( 'clipperServer.autoStart' ) ) {
2021-06-22 20:57:04 +02:00
void ClipperServer . instance ( ) . start ( ) ;
2020-10-09 19:35:46 +02:00
}
ExternalEditWatcher . instance ( ) . setLogger ( reg . logger ( ) ) ;
2020-11-16 13:03:44 +02:00
ExternalEditWatcher . instance ( ) . initialize ( bridge , this . store ( ) . dispatch ) ;
2020-10-09 19:35:46 +02:00
2020-11-12 21:13:28 +02:00
ResourceEditWatcher . instance ( ) . initialize ( reg . logger ( ) , ( action : any ) = > { this . store ( ) . dispatch ( action ) ; } ) ;
2020-10-09 19:35:46 +02:00
RevisionService . instance ( ) . runInBackground ( ) ;
// Make it available to the console window - useful to call revisionService.collectRevisions()
2021-05-13 18:57:37 +02:00
if ( Setting . value ( 'env' ) === 'dev' ) {
( window as any ) . joplin = {
2020-10-09 19:35:46 +02:00
revisionService : RevisionService.instance ( ) ,
migrationService : MigrationService.instance ( ) ,
decryptionWorker : DecryptionWorker.instance ( ) ,
2021-01-08 20:06:33 +02:00
commandService : CommandService.instance ( ) ,
2020-10-09 19:35:46 +02:00
bridge : bridge ( ) ,
2021-05-13 18:57:37 +02:00
debug : new DebugService ( reg . db ( ) ) ,
2020-10-09 19:35:46 +02:00
} ;
2021-05-13 18:57:37 +02:00
}
2020-10-09 19:35:46 +02:00
bridge ( ) . addEventListener ( 'nativeThemeUpdated' , this . bridge_nativeThemeUpdated ) ;
2020-11-13 19:09:28 +02:00
await this . initPluginService ( ) ;
2020-10-09 19:35:46 +02:00
2020-11-05 18:58:23 +02:00
this . setupContextMenu ( ) ;
await SpellCheckerService . instance ( ) . initialize ( new SpellCheckerServiceDriverNative ( ) ) ;
2020-10-09 19:35:46 +02:00
// await populateDatabase(reg.db());
// setTimeout(() => {
// console.info(CommandService.instance().commandsToMarkdownTable(this.store().getState()));
// }, 2000);
2021-01-22 02:55:58 +02:00
// setTimeout(() => {
// this.dispatch({
// type: 'NAV_GO',
// routeName: 'Config',
// props: {
2021-08-17 13:03:19 +02:00
// defaultSection: 'encryption',
2021-01-22 02:55:58 +02:00
// },
// });
2021-08-17 13:03:19 +02:00
// }, 2000);
2020-11-19 14:34:49 +02:00
2021-08-16 16:20:14 +02:00
// setTimeout(() => {
// this.dispatch({
// type: 'DIALOG_OPEN',
// name: 'syncWizard',
// });
// }, 2000);
2021-09-01 13:17:20 +02:00
// setTimeout(() => {
// this.dispatch({
// type: 'NAV_GO',
// routeName: 'Config',
// props: {
// defaultSection: 'plugins',
// },
// });
// }, 2000);
2020-10-09 19:35:46 +02:00
return null ;
}
}
2020-11-12 21:13:28 +02:00
let application_ : Application = null ;
2020-10-09 19:35:46 +02:00
function app() {
if ( ! application_ ) application_ = new Application ( ) ;
return application_ ;
}
export default app ;