1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-06-09 22:47:35 +02:00

Refactoring so that CLI app and Electron app share the same base application

This commit is contained in:
Laurent Cozic 2017-11-04 12:23:46 +00:00
parent f1e1db6744
commit 6e4effdecf
14 changed files with 517 additions and 524 deletions

View File

@ -1,3 +1,4 @@
const { BaseApplication } = require('lib/BaseApplication');
const { createStore, applyMiddleware } = require('redux'); const { createStore, applyMiddleware } = require('redux');
const { reducer, defaultState } = require('lib/reducer.js'); const { reducer, defaultState } = require('lib/reducer.js');
const { JoplinDatabase } = require('lib/joplin-database.js'); const { JoplinDatabase } = require('lib/joplin-database.js');
@ -14,68 +15,35 @@ const { Logger } = require('lib/logger.js');
const { sprintf } = require('sprintf-js'); const { sprintf } = require('sprintf-js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { fileExtension } = require('lib/path-utils.js'); const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js'); const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const os = require('os'); const os = require('os');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js'); const { cliUtils } = require('./cli-utils.js');
const EventEmitter = require('events'); const EventEmitter = require('events');
class Application { class Application extends BaseApplication {
constructor() { constructor() {
super();
this.showPromptString_ = true; this.showPromptString_ = true;
this.logger_ = new Logger();
this.dbLogger_ = new Logger();
this.commands_ = {}; this.commands_ = {};
this.commandMetadata_ = null; this.commandMetadata_ = null;
this.activeCommand_ = null; this.activeCommand_ = null;
this.allCommandsLoaded_ = false; this.allCommandsLoaded_ = false;
this.showStackTraces_ = false; this.showStackTraces_ = false;
this.gui_ = null; this.gui_ = null;
this.eventEmitter_ = new EventEmitter();
// Note: this is basically a cache of state.selectedFolderId. It should *only*
// be derived from the state and not set directly since that would make the
// state and UI out of sync.
this.currentFolder_ = null;
} }
gui() { gui() {
return this.gui_; return this.gui_;
} }
logger() {
return this.logger_;
}
store() {
return this.store_;
}
currentFolder() {
return this.currentFolder_;
}
commandStdoutMaxWidth() { commandStdoutMaxWidth() {
return this.gui().stdoutMaxWidth(); return this.gui().stdoutMaxWidth();
} }
async refreshCurrentFolder() {
let newFolder = null;
if (this.currentFolder_) newFolder = await Folder.load(this.currentFolder_.id);
if (!newFolder) newFolder = await Folder.defaultFolder();
this.switchCurrentFolder(newFolder);
}
switchCurrentFolder(folder) {
this.dispatch({
type: 'FOLDERS_SELECT',
id: folder ? folder.id : '',
});
}
async guessTypeAndLoadItem(pattern, options = null) { async guessTypeAndLoadItem(pattern, options = null) {
let type = BaseModel.TYPE_NOTE; let type = BaseModel.TYPE_NOTE;
if (pattern.indexOf('/') === 0) { if (pattern.indexOf('/') === 0) {
@ -149,112 +117,6 @@ class Application {
return []; return [];
} }
// Handles the initial flags passed to main script and
// returns the remaining args.
async handleStartFlags_(argv) {
let matched = {};
argv = argv.slice(0);
argv.splice(0, 2); // First arguments are the node executable, and the node JS file
while (argv.length) {
let arg = argv[0];
let nextArg = argv.length >= 2 ? argv[1] : null;
if (arg == '--profile') {
if (!nextArg) throw new Error(_('Usage: %s', '--profile <dir-path>'));
matched.profileDir = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--env') {
if (!nextArg) throw new Error(_('Usage: %s', '--env <dev|prod>'));
matched.env = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--is-demo') {
Setting.setConstant('isDemo', true);
argv.splice(0, 1);
continue;
}
if (arg == '--update-geolocation-disabled') {
Note.updateGeolocationEnabled_ = false;
argv.splice(0, 1);
continue;
}
if (arg == '--stack-trace-enabled') {
this.showStackTraces_ = true;
argv.splice(0, 1);
continue;
}
if (arg == '--log-level') {
if (!nextArg) throw new Error(_('Usage: %s', '--log-level <none|error|warn|info|debug>'));
matched.logLevel = Logger.levelStringToId(nextArg);
argv.splice(0, 2);
continue;
}
if (arg.length && arg[0] == '-') {
throw new Error(_('Unknown flag: %s', arg));
} else {
break;
}
}
if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO;
if (!matched.env) matched.env = 'prod';
return {
matched: matched,
argv: argv,
};
}
escapeShellArg(arg) {
if (arg.indexOf('"') >= 0 && arg.indexOf("'") >= 0) throw new Error(_('Command line argument "%s" contains both quotes and double-quotes - aborting.', arg)); // Hopeless case
let quote = '"';
if (arg.indexOf('"') >= 0) quote = "'";
if (arg.indexOf(' ') >= 0 || arg.indexOf("\t") >= 0) return quote + arg + quote;
return arg;
}
shellArgsToString(args) {
let output = [];
for (let i = 0; i < args.length; i++) {
output.push(this.escapeShellArg(args[i]));
}
return output.join(' ');
}
onLocaleChanged() {
return;
let currentCommands = this.vorpal().commands;
for (let i = 0; i < currentCommands.length; i++) {
let cmd = currentCommands[i];
if (cmd._name == 'help') {
cmd.description(_('Provides help for a given command.'));
} else if (cmd._name == 'exit') {
cmd.description(_('Exits the application.'));
} else if (cmd.__commandObject) {
cmd.description(cmd.__commandObject.description());
}
}
}
baseModelListener(action) {
this.eventEmitter_.emit('modelAction', { action: action });
}
on(eventName, callback) {
return this.eventEmitter_.on(eventName, callback);
}
stdout(text) { stdout(text) {
return this.gui().stdout(text); return this.gui().stdout(text);
} }
@ -297,9 +159,8 @@ class Application {
async exit(code = 0) { async exit(code = 0) {
const doExit = async () => { const doExit = async () => {
await Setting.saveAll();
this.gui().exit(); this.gui().exit();
process.exit(code); await super.exit(code);
}; };
// Give it a few seconds to cancel otherwise exit anyway // Give it a few seconds to cancel otherwise exit anyway
@ -429,174 +290,13 @@ class Application {
return this.activeCommand_; return this.activeCommand_;
} }
async refreshNotes(parentType, parentId) { async start(argv) {
this.logger().debug('Refreshing notes:', parentType, parentId); argv = await super.start(argv);
const state = this.store().getState();
let options = {
order: state.notesOrder,
uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'),
};
const source = JSON.stringify({
options: options,
parentId: parentId,
});
let notes = [];
if (parentId) {
if (parentType === Folder.modelType()) {
notes = await Note.previews(parentId, options);
} else if (parentType === Tag.modelType()) {
notes = await Tag.notes(parentId);
} else if (parentType === BaseModel.TYPE_SEARCH) {
let fields = Note.previewFields();
let search = BaseModel.byId(state.searches, parentId);
notes = await Note.previews(null, {
fields: fields,
anywherePattern: '*' + search.query_pattern + '*',
});
}
}
this.store().dispatch({
type: 'NOTES_UPDATE_ALL',
notes: notes,
notesSource: source,
});
this.store().dispatch({
type: 'NOTES_SELECT',
noteId: notes.length ? notes[0].id : null,
});
}
reducerActionToString(action) {
let o = [action.type];
if (action.id) o.push(action.id);
if (action.noteId) o.push(action.noteId);
if (action.folderId) o.push(action.folderId);
if (action.tagId) o.push(action.tagId);
if (action.tag) o.push(action.tag.id);
if (action.folder) o.push(action.folder.id);
if (action.notesSource) o.push(JSON.stringify(action.notesSource));
return o.join(', ');
}
generalMiddleware() {
const middleware = store => next => async (action) => {
this.logger().debug('Reducer action', this.reducerActionToString(action));
const result = next(action);
const newState = store.getState();
if (action.type == 'FOLDERS_SELECT' || action.type === 'FOLDER_DELETE') {
Setting.setValue('activeFolderId', newState.selectedFolderId);
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
await this.refreshNotes(Folder.modelType(), newState.selectedFolderId);
}
if (action.type == 'TAGS_SELECT') {
await this.refreshNotes(Tag.modelType(), action.id);
}
if (action.type == 'SEARCH_SELECT') {
await this.refreshNotes(BaseModel.TYPE_SEARCH, action.id);
}
if (this.gui() && action.type == 'SETTINGS_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTINGS_UPDATE_ALL') {
reg.setupRecurrentSync();
}
return result;
}
return middleware;
}
dispatch(action) {
if (this.store()) return this.store().dispatch(action);
}
async start() {
let argv = process.argv;
let startFlags = await this.handleStartFlags_(argv);
argv = startFlags.argv;
let initArgs = startFlags.matched;
if (argv.length) this.showPromptString_ = false;
if (process.argv[1].indexOf('joplindev') >= 0) {
if (!initArgs.profileDir) initArgs.profileDir = '/mnt/d/Temp/TestNotes2';
initArgs.logLevel = Logger.LEVEL_DEBUG;
initArgs.env = 'dev';
}
Setting.setConstant('appName', initArgs.env == 'dev' ? 'joplindev' : 'joplin');
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
const resourceDir = profileDir + '/resources';
const tempDir = profileDir + '/tmp';
Setting.setConstant('env', initArgs.env);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
await fs.mkdirp(profileDir, 0o755);
await fs.mkdirp(resourceDir, 0o755);
await fs.mkdirp(tempDir, 0o755);
this.logger_.addTarget('file', { path: profileDir + '/log.txt' });
this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_);
reg.dispatch = (o) => {};
this.dbLogger_.addTarget('file', { path: profileDir + '/log-database.txt' });
this.dbLogger_.setLevel(initArgs.logLevel);
if (Setting.value('env') === 'dev') {
this.dbLogger_.setLevel(Logger.LEVEL_WARN);
}
const packageJson = require('./package.json');
this.logger_.info(sprintf('Starting %s %s (%s)...', packageJson.name, packageJson.version, Setting.value('env')));
this.logger_.info('Profile directory: ' + profileDir);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
//this.database_.setLogExcludedQueryTypes(['SELECT']);
this.database_.setLogger(this.dbLogger_);
await this.database_.open({ name: profileDir + '/database.sqlite' });
reg.setDb(this.database_);
BaseModel.db_ = this.database_;
cliUtils.setStdout((object) => { cliUtils.setStdout((object) => {
return this.stdout(object); return this.stdout(object);
}); });
await Setting.load();
if (Setting.value('firstStart')) {
let locale = process.env.LANG;
if (!locale) locale = defaultLocale();
locale = locale.split('.');
locale = locale[0];
reg.logger().info('First start: detected locale as ' + locale);
Setting.setValue('locale', closestSupportedLocale(locale));
Setting.setValue('firstStart', 0)
}
setLocale(Setting.value('locale'));
let currentFolderId = Setting.value('activeFolderId');
let currentFolder = null;
if (currentFolderId) currentFolder = await Folder.load(currentFolderId);
if (!currentFolder) currentFolder = await Folder.defaultFolder();
Setting.setValue('activeFolderId', currentFolder ? currentFolder.id : '');
// If we have some arguments left at this point, it's a command // If we have some arguments left at this point, it's a command
// so execute it. // so execute it.
if (argv.length) { if (argv.length) {

View File

@ -48,19 +48,6 @@ if (process.platform === "win32") {
}); });
} }
// let commandCancelCalled_ = false;
// process.on("SIGINT", async function() {
// const cmd = application.currentCommand();
// if (!cmd || !cmd.cancellable() || commandCancelCalled_) {
// process.exit(0);
// } else {
// commandCancelCalled_ = true;
// await cmd.cancel();
// }
// });
process.stdout.on('error', function( err ) { process.stdout.on('error', function( err ) {
// https://stackoverflow.com/questions/12329816/error-write-epipe-when-piping-node-output-to-head#15884508 // https://stackoverflow.com/questions/12329816/error-write-epipe-when-piping-node-output-to-head#15884508
if (err.code == "EPIPE") { if (err.code == "EPIPE") {
@ -68,7 +55,7 @@ process.stdout.on('error', function( err ) {
} }
}); });
application.start().catch((error) => { application.start(process.argv).catch((error) => {
console.error(_('Fatal error:')); console.error(_('Fatal error:'));
console.error(error); console.error(error);
}); });

View File

@ -3,7 +3,7 @@
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BUILD_DIR="$ROOT_DIR/build" BUILD_DIR="$ROOT_DIR/build"
rsync -a "$ROOT_DIR/app/" "$BUILD_DIR/" rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/" rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
cp "$ROOT_DIR/package.json" "$BUILD_DIR" cp "$ROOT_DIR/package.json" "$BUILD_DIR"
chmod 755 "$BUILD_DIR/main.js" chmod 755 "$BUILD_DIR/main.js"

View File

@ -85,26 +85,6 @@ msgstr ""
msgid "No notebook has been specified." msgid "No notebook has been specified."
msgstr "" msgstr ""
#, javascript-format
msgid "Usage: %s"
msgstr ""
#, javascript-format
msgid "Unknown flag: %s"
msgstr ""
#, javascript-format
msgid ""
"Command line argument \"%s\" contains both quotes and double-quotes - "
"aborting."
msgstr ""
msgid "Provides help for a given command."
msgstr ""
msgid "Exits the application."
msgstr ""
msgid "Y" msgid "Y"
msgstr "" msgstr ""
@ -205,6 +185,9 @@ msgstr ""
msgid "Note has been saved." msgid "Note has been saved."
msgstr "" msgstr ""
msgid "Exits the application."
msgstr ""
msgid "" msgid ""
"Exports Joplin data to the given directory. By default, it will export the " "Exports Joplin data to the given directory. By default, it will export the "
"complete database including notebooks, notes, tags and resources." "complete database including notebooks, notes, tags and resources."
@ -487,6 +470,14 @@ msgid ""
"will be shared with any third party." "will be shared with any third party."
msgstr "" msgstr ""
#, javascript-format
msgid "Usage: %s"
msgstr ""
#, javascript-format
msgid "Unknown flag: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unknown log level: %s" msgid "Unknown log level: %s"
msgstr "" msgstr ""

View File

@ -85,28 +85,6 @@ msgstr "Aucun carnet n'est sélectionné."
msgid "No notebook has been specified." msgid "No notebook has been specified."
msgstr "Aucun carnet n'est spécifié." msgstr "Aucun carnet n'est spécifié."
#, javascript-format
msgid "Usage: %s"
msgstr "Utilisation : %s"
#, javascript-format
msgid "Unknown flag: %s"
msgstr "Paramètre inconnu : %s"
#, javascript-format
msgid ""
"Command line argument \"%s\" contains both quotes and double-quotes - "
"aborting."
msgstr ""
"Le paramètre de ligne de commande \"%s\" contient à la fois des guillemets "
"simples et doubles - impossible de continuer."
msgid "Provides help for a given command."
msgstr "Affiche l'aide pour la commande donnée."
msgid "Exits the application."
msgstr "Quitter le logiciel."
msgid "Y" msgid "Y"
msgstr "O" msgstr "O"
@ -217,6 +195,9 @@ msgstr ""
msgid "Note has been saved." msgid "Note has been saved."
msgstr "La note a été enregistrée." msgstr "La note a été enregistrée."
msgid "Exits the application."
msgstr "Quitter le logiciel."
msgid "" msgid ""
"Exports Joplin data to the given directory. By default, it will export the " "Exports Joplin data to the given directory. By default, it will export the "
"complete database including notebooks, notes, tags and resources." "complete database including notebooks, notes, tags and resources."
@ -539,6 +520,14 @@ msgstr ""
"aucun fichier en dehors de ce répertoire, ni à d'autres données " "aucun fichier en dehors de ce répertoire, ni à d'autres données "
"personnelles. Aucune donnée ne sera partagé avec aucun tier." "personnelles. Aucune donnée ne sera partagé avec aucun tier."
#, javascript-format
msgid "Usage: %s"
msgstr "Utilisation : %s"
#, javascript-format
msgid "Unknown flag: %s"
msgstr "Paramètre inconnu : %s"
#, javascript-format #, javascript-format
msgid "Unknown log level: %s" msgid "Unknown log level: %s"
msgstr "Paramètre inconnu : %s" msgstr "Paramètre inconnu : %s"
@ -829,6 +818,16 @@ msgstr ""
msgid "Welcome" msgid "Welcome"
msgstr "Bienvenue" msgstr "Bienvenue"
#~ msgid ""
#~ "Command line argument \"%s\" contains both quotes and double-quotes - "
#~ "aborting."
#~ msgstr ""
#~ "Le paramètre de ligne de commande \"%s\" contient à la fois des "
#~ "guillemets simples et doubles - impossible de continuer."
#~ msgid "Provides help for a given command."
#~ msgstr "Affiche l'aide pour la commande donnée."
#, fuzzy #, fuzzy
#~ msgid "Create a [n]ew [n]otebook" #~ msgid "Create a [n]ew [n]otebook"
#~ msgstr "Créer un carnet." #~ msgstr "Créer un carnet."

View File

@ -85,26 +85,6 @@ msgstr ""
msgid "No notebook has been specified." msgid "No notebook has been specified."
msgstr "" msgstr ""
#, javascript-format
msgid "Usage: %s"
msgstr ""
#, javascript-format
msgid "Unknown flag: %s"
msgstr ""
#, javascript-format
msgid ""
"Command line argument \"%s\" contains both quotes and double-quotes - "
"aborting."
msgstr ""
msgid "Provides help for a given command."
msgstr ""
msgid "Exits the application."
msgstr ""
msgid "Y" msgid "Y"
msgstr "" msgstr ""
@ -205,6 +185,9 @@ msgstr ""
msgid "Note has been saved." msgid "Note has been saved."
msgstr "" msgstr ""
msgid "Exits the application."
msgstr ""
msgid "" msgid ""
"Exports Joplin data to the given directory. By default, it will export the " "Exports Joplin data to the given directory. By default, it will export the "
"complete database including notebooks, notes, tags and resources." "complete database including notebooks, notes, tags and resources."
@ -487,6 +470,14 @@ msgid ""
"will be shared with any third party." "will be shared with any third party."
msgstr "" msgstr ""
#, javascript-format
msgid "Usage: %s"
msgstr ""
#, javascript-format
msgid "Unknown flag: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unknown log level: %s" msgid "Unknown log level: %s"
msgstr "" msgstr ""

109
ElectronClient/app/app.js Normal file
View File

@ -0,0 +1,109 @@
const { BaseApplication } = require('lib/BaseApplication');
const { BrowserWindow } = require('electron');
const { Setting } = require('lib/models/setting.js');
const { BaseModel } = require('lib/base-model.js');
const { _ } = require('lib/locale.js');
const path = require('path')
const url = require('url')
const os = require('os');
const fs = require('fs-extra');
const { Logger } = require('lib/logger.js');
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');
class Application extends BaseApplication {
constructor(electronApp) {
super();
this.electronApp_ = electronApp;
this.loadState_ = 'start';
this.win_ = null;
this.electronApp_.on('ready', () => {
this.loadState_ = 'ready';
});
}
createWindow() {
// Create the browser window.
this.win_ = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
this.win_.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}))
// Open the DevTools.
this.win_.webContents.openDevTools()
// Emitted when the window is closed.
this.win_.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
this.win_ = null
})
}
waitForElectronAppReady() {
if (this.loadState_ === 'ready') return Promise.resolve();
return new Promise((resolve, reject) => {
const iid = setInterval(() => {
if (this.loadState_ === 'ready') {
clearInterval(iid);
resolve();
}
}, 10);
});
}
async start(argv) {
argv = await super.start(argv);
// Since we are doing other async things before creating the window, we might miss
// the "ready" event. So we use the function below to make sure that the app is
// ready.
await this.waitForElectronAppReady();
this.createWindow();
// Quit when all windows are closed.
this.electronApp_.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
this.electronApp_.quit()
}
})
this.electronApp_.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (this.win_ === null) {
createWindow()
}
})
}
}
let application_ = null;
function app() {
if (!application_) throw new Error('Application has not been initialized');
return application_;
}
function initApp(electronApp) {
if (application_) throw new Error('Application has already been initialized');
application_ = new Application(electronApp);
return application_;
}
module.exports = { app, initApp };

View File

@ -1,150 +1,17 @@
// Make it possible to require("/lib/...") without specifying full path // Make it possible to require("/lib/...") without specifying full path
//require('app-module-path').addPath(__dirname + '/../ReactNativeClient');
require('app-module-path').addPath(__dirname); require('app-module-path').addPath(__dirname);
const electronApp = require('electron').app; const electronApp = require('electron').app;
const { BrowserWindow } = require('electron'); const { initApp } = require('./app');
const { Setting } = require('lib/models/setting.js');
const { BaseModel } = require('lib/base-model.js');
const { _ } = require('lib/locale.js');
const path = require('path')
const url = require('url')
const os = require('os');
const fs = require('fs-extra');
const { Logger } = require('lib/logger.js');
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');
class Application {
constructor(electronApp) {
this.electronApp_ = electronApp;
this.loadState_ = 'start';
this.win_ = null;
this.logger_ = new Logger();
this.dbLogger_ = new Logger();
this.database_ = null;
this.electronApp_.on('ready', () => {
this.loadState_ = 'ready';
})
}
createWindow() {
// Create the browser window.
this.win_ = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
this.win_.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}))
// Open the DevTools.
this.win_.webContents.openDevTools()
// Emitted when the window is closed.
this.win_.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
this.win_ = null
})
}
waitForElectronAppReady() {
if (this.loadState_ === 'ready') return Promise.resolve();
return new Promise((resolve, reject) => {
const iid = setInterval(() => {
if (this.loadState_ === 'ready') {
clearInterval(iid);
resolve();
}
}, 10);
});
}
async start() {
let initArgs = { env: 'dev', logLevel: Logger.LEVEL_DEBUG };
Setting.setConstant('appName', initArgs.env == 'dev' ? 'joplindev-desktop' : 'joplin-desktop');
const profileDir = os.homedir() + '/.config/' + Setting.value('appName');
const resourceDir = profileDir + '/resources';
const tempDir = profileDir + '/tmp';
Setting.setConstant('env', initArgs.env);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
await fs.mkdirp(profileDir, 0o755);
await fs.mkdirp(resourceDir, 0o755);
await fs.mkdirp(tempDir, 0o755);
this.logger_.addTarget('file', { path: profileDir + '/log.txt' });
this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_);
reg.dispatch = (o) => {};
this.dbLogger_.addTarget('file', { path: profileDir + '/log-database.txt' });
this.dbLogger_.setLevel(initArgs.logLevel);
if (Setting.value('env') === 'dev') {
//this.dbLogger_.setLevel(Logger.LEVEL_WARN);
}
const packageJson = require('./package.json');
this.logger_.info(sprintf('Starting %s %s (%s)...', packageJson.name, packageJson.version, Setting.value('env')));
this.logger_.info('Profile directory: ' + profileDir);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
//this.database_.setLogExcludedQueryTypes(['SELECT']);
this.database_.setLogger(this.dbLogger_);
await this.database_.open({ name: profileDir + '/database.sqlite' });
reg.setDb(this.database_);
BaseModel.db_ = this.database_;
// Since we are doing other async things before creating the window, we might miss
// the "ready" event. So we use the function below to make sure that the app is
// ready.
await this.waitForElectronAppReady();
this.createWindow();
// Quit when all windows are closed.
this.electronApp_.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
this.electronApp_.quit()
}
})
this.electronApp_.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (this.win_ === null) {
createWindow()
}
})
}
}
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled promise rejection', p, 'reason:', reason); console.error('Unhandled promise rejection', p, 'reason:', reason);
process.exit(1); process.exit(1);
}); });
const app = new Application(electronApp); const app = initApp(electronApp);
app.start().catch((error) => {
app.start(process.argv).catch((error) => {
console.error(_('Fatal error:')); console.error(_('Fatal error:'));
console.error(error); console.error(error);
}); });

View File

@ -1586,6 +1586,11 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true "dev": true
}, },
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-yaml": { "js-yaml": {
"version": "3.10.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz",
@ -1708,6 +1713,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
}, },
"lodash-es": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
"integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
},
"lodash.assign": { "lodash.assign": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
@ -1756,6 +1766,14 @@
} }
} }
}, },
"loose-envify": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
"requires": {
"js-tokens": "3.0.2"
}
},
"loud-rejection": { "loud-rejection": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
@ -2465,6 +2483,17 @@
"strip-indent": "1.0.1" "strip-indent": "1.0.1"
} }
}, },
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"requires": {
"lodash": "4.17.4",
"lodash-es": "4.17.4",
"loose-envify": "1.3.1",
"symbol-observable": "1.0.4"
}
},
"registry-auth-token": { "registry-auth-token": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.1.tgz", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.1.tgz",
@ -3560,8 +3589,7 @@
"symbol-observable": { "symbol-observable": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz",
"integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=", "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0="
"dev": true
}, },
"tar": { "tar": {
"version": "2.2.1", "version": "2.2.1",

View File

@ -29,6 +29,7 @@
"moment": "^2.19.1", "moment": "^2.19.1",
"promise": "^8.0.1", "promise": "^8.0.1",
"query-string": "^5.0.1", "query-string": "^5.0.1",
"redux": "^3.7.2",
"sprintf-js": "^1.1.1", "sprintf-js": "^1.1.1",
"sqlite3": "^3.1.13" "sqlite3": "^3.1.13"
} }

View File

@ -1,4 +1,6 @@
#!/bin/bash #!/bin/bash
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$ROOT_DIR"
./build.sh
cd "$ROOT_DIR/build" cd "$ROOT_DIR/build"
node_modules/.bin/electron . node_modules/.bin/electron .

View File

@ -0,0 +1,305 @@
const { createStore, applyMiddleware } = require('redux');
const { reducer, defaultState } = require('lib/reducer.js');
const { JoplinDatabase } = require('lib/joplin-database.js');
const { Database } = require('lib/database.js');
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { BaseItem } = require('lib/models/base-item.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
const { Setting } = require('lib/models/setting.js');
const { Logger } = require('lib/logger.js');
const { sprintf } = require('sprintf-js');
const { reg } = require('lib/registry.js');
const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const os = require('os');
const fs = require('fs-extra');
const EventEmitter = require('events');
class BaseApplication {
constructor() {
this.logger_ = new Logger();
this.dbLogger_ = new Logger();
this.eventEmitter_ = new EventEmitter();
// Note: this is basically a cache of state.selectedFolderId. It should *only*
// be derived from the state and not set directly since that would make the
// state and UI out of sync.
this.currentFolder_ = null;
}
logger() {
return this.logger_;
}
store() {
return this.store_;
}
currentFolder() {
return this.currentFolder_;
}
async refreshCurrentFolder() {
let newFolder = null;
if (this.currentFolder_) newFolder = await Folder.load(this.currentFolder_.id);
if (!newFolder) newFolder = await Folder.defaultFolder();
this.switchCurrentFolder(newFolder);
}
switchCurrentFolder(folder) {
this.dispatch({
type: 'FOLDERS_SELECT',
id: folder ? folder.id : '',
});
}
// Handles the initial flags passed to main script and
// returns the remaining args.
async handleStartFlags_(argv) {
let matched = {};
argv = argv.slice(0);
argv.splice(0, 2); // First arguments are the node executable, and the node JS file
while (argv.length) {
let arg = argv[0];
let nextArg = argv.length >= 2 ? argv[1] : null;
if (arg == '--profile') {
if (!nextArg) throw new Error(_('Usage: %s', '--profile <dir-path>'));
matched.profileDir = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--env') {
if (!nextArg) throw new Error(_('Usage: %s', '--env <dev|prod>'));
matched.env = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--is-demo') {
Setting.setConstant('isDemo', true);
argv.splice(0, 1);
continue;
}
if (arg == '--update-geolocation-disabled') {
Note.updateGeolocationEnabled_ = false;
argv.splice(0, 1);
continue;
}
if (arg == '--stack-trace-enabled') {
this.showStackTraces_ = true;
argv.splice(0, 1);
continue;
}
if (arg == '--log-level') {
if (!nextArg) throw new Error(_('Usage: %s', '--log-level <none|error|warn|info|debug>'));
matched.logLevel = Logger.levelStringToId(nextArg);
argv.splice(0, 2);
continue;
}
if (arg.length && arg[0] == '-') {
throw new Error(_('Unknown flag: %s', arg));
} else {
break;
}
}
if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO;
if (!matched.env) matched.env = 'prod';
return {
matched: matched,
argv: argv,
};
}
on(eventName, callback) {
return this.eventEmitter_.on(eventName, callback);
}
async exit(code = 0) {
await Setting.saveAll();
process.exit(code);
}
async refreshNotes(parentType, parentId) {
this.logger().debug('Refreshing notes:', parentType, parentId);
const state = this.store().getState();
let options = {
order: state.notesOrder,
uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'),
};
const source = JSON.stringify({
options: options,
parentId: parentId,
});
let notes = [];
if (parentId) {
if (parentType === Folder.modelType()) {
notes = await Note.previews(parentId, options);
} else if (parentType === Tag.modelType()) {
notes = await Tag.notes(parentId);
} else if (parentType === BaseModel.TYPE_SEARCH) {
let fields = Note.previewFields();
let search = BaseModel.byId(state.searches, parentId);
notes = await Note.previews(null, {
fields: fields,
anywherePattern: '*' + search.query_pattern + '*',
});
}
}
this.store().dispatch({
type: 'NOTES_UPDATE_ALL',
notes: notes,
notesSource: source,
});
this.store().dispatch({
type: 'NOTES_SELECT',
noteId: notes.length ? notes[0].id : null,
});
}
reducerActionToString(action) {
let o = [action.type];
if (action.id) o.push(action.id);
if (action.noteId) o.push(action.noteId);
if (action.folderId) o.push(action.folderId);
if (action.tagId) o.push(action.tagId);
if (action.tag) o.push(action.tag.id);
if (action.folder) o.push(action.folder.id);
if (action.notesSource) o.push(JSON.stringify(action.notesSource));
return o.join(', ');
}
generalMiddleware() {
const middleware = store => next => async (action) => {
this.logger().debug('Reducer action', this.reducerActionToString(action));
const result = next(action);
const newState = store.getState();
if (action.type == 'FOLDERS_SELECT' || action.type === 'FOLDER_DELETE') {
Setting.setValue('activeFolderId', newState.selectedFolderId);
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
await this.refreshNotes(Folder.modelType(), newState.selectedFolderId);
}
if (action.type == 'TAGS_SELECT') {
await this.refreshNotes(Tag.modelType(), action.id);
}
if (action.type == 'SEARCH_SELECT') {
await this.refreshNotes(BaseModel.TYPE_SEARCH, action.id);
}
if (this.gui() && action.type == 'SETTINGS_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTINGS_UPDATE_ALL') {
reg.setupRecurrentSync();
}
return result;
}
return middleware;
}
dispatch(action) {
if (this.store()) return this.store().dispatch(action);
}
async start(argv) {
let startFlags = await this.handleStartFlags_(argv);
argv = startFlags.argv;
let initArgs = startFlags.matched;
if (argv.length) this.showPromptString_ = false;
if (process.argv[1].indexOf('joplindev') >= 0) {
if (!initArgs.profileDir) initArgs.profileDir = '/mnt/d/Temp/TestNotes2';
initArgs.logLevel = Logger.LEVEL_DEBUG;
initArgs.env = 'dev';
}
Setting.setConstant('appName', initArgs.env == 'dev' ? 'joplindev' : 'joplin');
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
const resourceDir = profileDir + '/resources';
const tempDir = profileDir + '/tmp';
Setting.setConstant('env', initArgs.env);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
console.info(Setting.value('env'), Setting.value('profileDir'), Setting.value('resourceDir'), Setting.value('tempDir'));
await fs.mkdirp(profileDir, 0o755);
await fs.mkdirp(resourceDir, 0o755);
await fs.mkdirp(tempDir, 0o755);
this.logger_.addTarget('file', { path: profileDir + '/log.txt' });
this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_);
reg.dispatch = (o) => {};
this.dbLogger_.addTarget('file', { path: profileDir + '/log-database.txt' });
this.dbLogger_.setLevel(initArgs.logLevel);
if (Setting.value('env') === 'dev') {
this.dbLogger_.setLevel(Logger.LEVEL_WARN);
}
const packageJson = require('./package.json');
this.logger_.info(sprintf('Starting %s %s (%s)...', packageJson.name, packageJson.version, Setting.value('env')));
this.logger_.info('Profile directory: ' + profileDir);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
//this.database_.setLogExcludedQueryTypes(['SELECT']);
this.database_.setLogger(this.dbLogger_);
await this.database_.open({ name: profileDir + '/database.sqlite' });
reg.setDb(this.database_);
BaseModel.db_ = this.database_;
await Setting.load();
if (Setting.value('firstStart')) {
const locale = shim.detectAndSetLocale(Setting);
reg.logger().info('First start: detected locale as ' + locale);
Setting.setValue('firstStart', 0)
} else {
setLocale(Setting.value('locale'));
}
let currentFolderId = Setting.value('activeFolderId');
let currentFolder = null;
if (currentFolderId) currentFolder = await Folder.load(currentFolderId);
if (!currentFolder) currentFolder = await Folder.defaultFolder();
Setting.setValue('activeFolderId', currentFolder ? currentFolder.id : '');
return argv;
}
}
module.exports = { BaseApplication };

View File

@ -3,6 +3,7 @@ const { shim } = require('lib/shim.js');
const { GeolocationNode } = require('lib/geolocation-node.js'); const { GeolocationNode } = require('lib/geolocation-node.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js'); const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
function fetchRequestCanBeRetried(error) { function fetchRequestCanBeRetried(error) {
if (!error) return false; if (!error) return false;
@ -41,6 +42,17 @@ function shimInit() {
shim.Geolocation = GeolocationNode; shim.Geolocation = GeolocationNode;
shim.FormData = require('form-data'); shim.FormData = require('form-data');
shim.detectAndSetLocale = function (Setting) {
let locale = process.env.LANG;
if (!locale) locale = defaultLocale();
locale = locale.split('.');
locale = locale[0];
locale = closestSupportedLocale(locale);
Setting.setValue('locale', locale);
setLocale(locale);
return locale;
}
const nodeFetch = require('node-fetch'); const nodeFetch = require('node-fetch');
shim.fetch = async function(url, options = null) { shim.fetch = async function(url, options = null) {

View File

@ -17,5 +17,6 @@ shim.readLocalFileBase64 = () => { throw new Error('Not implemented'); }
shim.uploadBlob = () => { throw new Error('Not implemented'); } shim.uploadBlob = () => { throw new Error('Not implemented'); }
shim.setInterval = setInterval; shim.setInterval = setInterval;
shim.clearInterval = clearInterval; shim.clearInterval = clearInterval;
shim.detectAndSetLocale = null;
module.exports = { shim }; module.exports = { shim };