keeweb/app/scripts/comp/launcher/launcher-electron.js

309 lines
9.6 KiB
JavaScript
Raw Normal View History

2019-09-16 22:57:56 +02:00
import { Events } from 'framework/events';
2019-09-15 14:16:32 +02:00
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
2019-09-18 07:08:23 +02:00
import { noop } from 'util/fn';
2015-10-22 20:03:44 +02:00
2017-02-05 11:08:26 +01:00
const logger = new Logger('launcher');
const Launcher = {
name: 'electron',
version: window.process.versions.electron,
autoTypeSupported: true,
2017-04-12 00:06:44 +02:00
thirdPartyStoragesSupported: true,
clipboardSupported: true,
2017-02-05 11:08:26 +01:00
req: window.require,
2019-08-18 10:17:09 +02:00
platform() {
2017-02-05 11:08:26 +01:00
return process.platform;
},
2019-08-18 10:17:09 +02:00
electron() {
2017-02-05 11:08:26 +01:00
return this.req('electron');
},
2019-08-18 10:17:09 +02:00
remoteApp() {
2017-02-05 11:08:26 +01:00
return this.electron().remote.app;
},
2019-08-18 10:17:09 +02:00
remReq(mod) {
2017-02-05 11:08:26 +01:00
return this.electron().remote.require(mod);
},
2019-08-18 10:17:09 +02:00
openLink(href) {
2017-02-05 11:08:26 +01:00
this.electron().shell.openExternal(href);
},
devTools: true,
2019-08-18 10:17:09 +02:00
openDevTools() {
2019-08-16 23:05:39 +02:00
this.electron()
.remote.getCurrentWindow()
.openDevTools({ mode: 'bottom' });
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
getSaveFileName(defaultPath, callback) {
2017-02-05 11:08:26 +01:00
if (defaultPath) {
const homePath = this.remReq('electron').app.getPath('userDesktop');
defaultPath = this.joinPath(homePath, defaultPath);
2017-02-05 11:08:26 +01:00
}
this.remReq('electron')
.dialog.showSaveDialog({
2019-08-16 23:05:39 +02:00
title: Locale.launcherSave,
2019-08-18 10:17:09 +02:00
defaultPath,
2019-08-16 23:05:39 +02:00
filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
})
.then(res => callback(res.filePath));
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
getUserDataPath(fileName) {
2017-06-05 13:33:12 +02:00
if (!this.userDataPath) {
const realUserDataPath = this.remoteApp().getPath('userData');
const suffixReplacementRegex = /[\\/]temp[\\/]\d+\.\d+[\\/]?$/;
this.userDataPath = realUserDataPath.replace(suffixReplacementRegex, '');
}
return this.joinPath(this.userDataPath, fileName || '');
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
getTempPath(fileName) {
return this.joinPath(this.remoteApp().getPath('temp'), fileName || '');
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
getDocumentsPath(fileName) {
return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
getAppPath(fileName) {
2017-12-02 20:38:13 +01:00
const dirname = this.req('path').dirname;
const appPath = __dirname.endsWith('app.asar') ? __dirname : this.remoteApp().getAppPath();
return this.joinPath(dirname(appPath), fileName || '');
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
getWorkDirPath(fileName) {
return this.joinPath(process.cwd(), fileName || '');
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
joinPath(...parts) {
2017-04-08 17:34:27 +02:00
return this.req('path').join(...parts);
},
2019-08-18 10:17:09 +02:00
writeFile(path, data, callback) {
2019-01-06 11:44:10 +01:00
this.req('fs').writeFile(path, window.Buffer.from(data), callback);
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
readFile(path, encoding, callback) {
2017-02-05 11:49:22 +01:00
this.req('fs').readFile(path, encoding, (err, contents) => {
2017-02-05 11:08:26 +01:00
const data = typeof contents === 'string' ? contents : new Uint8Array(contents);
2017-02-05 11:49:22 +01:00
callback(data, err);
});
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
fileExists(path, callback) {
2017-02-05 11:49:22 +01:00
this.req('fs').exists(path, callback);
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
deleteFile(path, callback) {
2019-09-18 07:08:23 +02:00
this.req('fs').unlink(path, callback || noop);
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
statFile(path, callback) {
2017-02-05 11:49:22 +01:00
this.req('fs').stat(path, (err, stats) => callback(stats, err));
},
2019-08-18 10:17:09 +02:00
mkdir(dir, callback) {
2017-02-05 11:08:26 +01:00
const fs = this.req('fs');
const path = this.req('path');
const stack = [];
2017-02-05 23:34:37 +01:00
const collect = function(dir, stack, callback) {
fs.exists(dir, exists => {
if (exists) {
return callback();
}
stack.unshift(dir);
const newDir = path.dirname(dir);
if (newDir === dir || !newDir || newDir === '.' || newDir === '/') {
return callback();
}
collect(newDir, stack, callback);
});
};
const create = function(stack, callback) {
if (!stack.length) {
2017-02-21 19:44:45 +01:00
return callback();
2017-02-05 11:08:26 +01:00
}
2017-02-05 23:34:37 +01:00
2019-08-16 23:05:39 +02:00
fs.mkdir(stack.shift(), err => (err ? callback(err) : create(stack, callback)));
2017-02-05 23:34:37 +01:00
};
collect(dir, stack, () => create(stack, callback));
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
parsePath(fileName) {
2017-02-05 11:08:26 +01:00
const path = this.req('path');
return {
path: fileName,
dir: path.dirname(fileName),
file: path.basename(fileName)
};
},
2019-08-18 10:17:09 +02:00
createFsWatcher(path) {
2017-02-05 11:08:26 +01:00
return this.req('fs').watch(path, { persistent: false });
},
2019-08-18 10:17:09 +02:00
ensureRunnable(path) {
2017-02-05 11:08:26 +01:00
if (process.platform !== 'win32') {
const fs = this.req('fs');
const stat = fs.statSync(path);
if ((stat.mode & 0o0111) === 0) {
const mode = stat.mode | 0o0100;
logger.info(`chmod 0${mode.toString(8)} ${path}`);
fs.chmodSync(path, mode);
}
}
},
2019-08-18 10:17:09 +02:00
preventExit(e) {
2017-02-05 11:08:26 +01:00
e.returnValue = false;
return false;
},
2019-08-18 10:17:09 +02:00
exit() {
2017-02-05 11:08:26 +01:00
this.exitRequested = true;
this.requestExit();
},
2019-08-18 10:17:09 +02:00
requestExit() {
2017-02-05 11:08:26 +01:00
const app = this.remoteApp();
if (this.restartPending) {
app.restartApp();
} else {
app.quit();
}
},
2019-08-18 10:17:09 +02:00
requestRestart() {
2017-02-05 11:08:26 +01:00
this.restartPending = true;
this.requestExit();
},
2019-08-18 10:17:09 +02:00
cancelRestart() {
2017-02-05 11:08:26 +01:00
this.restartPending = false;
},
2019-08-18 10:17:09 +02:00
setClipboardText(text) {
2017-02-05 11:08:26 +01:00
return this.electron().clipboard.writeText(text);
},
2019-08-18 10:17:09 +02:00
getClipboardText() {
2017-02-05 11:08:26 +01:00
return this.electron().clipboard.readText();
},
2019-08-18 10:17:09 +02:00
clearClipboardText() {
2017-02-05 11:08:26 +01:00
return this.electron().clipboard.clear();
},
2019-08-18 10:17:09 +02:00
minimizeApp() {
2019-10-05 08:37:10 +02:00
this.remoteApp().minimizeApp({
restore: Locale.menuRestoreApp.replace('{}', 'KeeWeb'),
quit: Locale.menuQuitApp.replace('{}', 'KeeWeb')
});
2017-02-05 11:08:26 +01:00
},
2019-08-18 10:17:09 +02:00
canMinimize() {
2017-02-05 11:08:26 +01:00
return process.platform !== 'darwin';
},
2019-08-18 10:17:09 +02:00
canDetectOsSleep() {
2017-06-12 21:07:09 +02:00
return process.platform !== 'linux';
},
2019-08-18 10:17:09 +02:00
updaterEnabled() {
2017-02-05 11:08:26 +01:00
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1;
},
2019-08-18 10:17:09 +02:00
getMainWindow() {
2017-02-05 11:08:26 +01:00
return this.remoteApp().getMainWindow();
},
2019-08-18 10:17:09 +02:00
resolveProxy(url, callback) {
2017-02-05 11:08:26 +01:00
const window = this.getMainWindow();
const session = window.webContents.session;
2019-10-27 16:43:47 +01:00
session.resolveProxy(url).then(proxy => {
2017-02-05 11:08:26 +01:00
const match = /^proxy\s+([\w\.]+):(\d+)+\s*/i.exec(proxy);
proxy = match && match[1] ? { host: match[1], port: +match[2] } : null;
callback(proxy);
});
},
2019-08-18 10:17:09 +02:00
openWindow(opts) {
2017-02-05 11:08:26 +01:00
return this.remoteApp().openWindow(opts);
},
2019-08-18 10:17:09 +02:00
hideApp() {
2017-02-05 11:08:26 +01:00
const app = this.remoteApp();
if (this.canMinimize()) {
tray-min-auto-type-select-fix The fix for alt-tab behavior when KeeWeb is minimized to the tray in 3dae878 left a problem when auto-type raises a selection list: the taskbar button shows, and after a selection is made KeeWeb minimizes to the taskbar but leaves a tray icon present. The same thing happens if auto-type is canceled by clicking either the minimize button or the close button at the top right of the selection window. From this state, various scenarios lead to having duplicate tray icons. This commit restores the behavior of 1.6.3 when auto-type raises a selection list while KeeWeb is minimized to the tray: the selection window shows, the tray icon stays, and no taskbar button shows. We used to minimize the window after selection regardless of its previous state; this worked because we hid the taskbar button and minimized the window when minimizing to the tray, but that's what caused the alt-tab problem. Since we now hide when minimizing to the tray, we have to know whether to minimize or hide after selection. The simplest way to do that is to keep the old behavior of leaving the tray icon present when auto-type raises a selection window while KeeWeb is minimized to the tray. Instead of calling minimize on the main window, launcher-electron.js now calls app.minimizeThenHideIfInTray which is defined in desktop/app.js. That routine minimizes KeeWeb (which returns focus to the previously active window) and then hides the main window if and only if a tray icon is present. Because we don't want a tray icon and a taskbar button present at the same time, app.minimizeApp is also changed to restore the call to mainWindow.setSkipTaskbar(true) in the non-Darwin path; thus, when auto-type raises a selection window, there won't be a taskbar button if KeeWeb was minimized to the tray. If auto-type is canceled by clicking the top right close button while a selection list is displayed and there is a tray icon, the KeeWeb window is hidden and the tray icon stays, just as one would expect. This is the most likely way someone using "Minimize app instead of close" would choose to dismiss the auto-type selection list. If auto-type is canceled when a selection list is displayed while there is a tray icon by clicking the top right minimize button, by using alt-tab, or by clicking outside the selection window, the KeeWeb window reverts to its normal display and shows in the alt-tab list, but the tray icon remains and no taskbar button is shown. This is not ideal; it could be addressed in another commit if it seems worth doing. This commit mitigates these scenarios by adding a check to app.minimizeApp to assure that we never create a second tray icon if one is already present. This can do no harm and might catch other "corner cases" that are difficult to foresee. The next time the tray icon is clicked or the app is minimized to the tray by clicking the top right close button normal behavior is fully restored. If I've made no mistakes, the only change to the Darwin path is that it, too, is subject to the check that a new tray icon is not created if one already exists. I'm guessing that's OK, but I have no way to test Darwin.
2018-09-05 05:29:45 +02:00
app.minimizeThenHideIfInTray();
2017-02-05 11:08:26 +01:00
} else {
app.hide();
}
},
2019-08-18 10:17:09 +02:00
isAppFocused() {
2017-02-05 11:08:26 +01:00
return !!this.electron().remote.BrowserWindow.getFocusedWindow();
},
2019-08-18 10:17:09 +02:00
showMainWindow() {
2017-02-05 11:08:26 +01:00
const win = this.getMainWindow();
2019-03-31 14:30:36 +02:00
win.show();
win.focus();
2017-02-05 11:08:26 +01:00
win.restore();
},
2019-08-18 10:17:09 +02:00
spawn(config) {
2017-02-05 11:08:26 +01:00
const ts = logger.ts();
let complete = config.complete;
const ps = this.req('child_process').spawn(config.cmd, config.args);
2017-02-05 11:08:26 +01:00
[ps.stdin, ps.stdout, ps.stderr].forEach(s => s.setEncoding('utf-8'));
let stderr = '';
let stdout = '';
2019-08-16 23:05:39 +02:00
ps.stderr.on('data', d => {
stderr += d.toString('utf-8');
});
ps.stdout.on('data', d => {
stdout += d.toString('utf-8');
});
2017-02-05 11:08:26 +01:00
ps.on('close', code => {
stdout = stdout.trim();
stderr = stderr.trim();
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
if (code) {
logger.error(msg + '\n' + stdout + '\n' + stderr);
} else {
logger.info(msg + (stdout ? '\n' + stdout : ''));
}
if (complete) {
complete(code ? 'Exit code ' + code : null, stdout, code);
complete = null;
}
});
ps.on('error', err => {
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), err);
if (complete) {
complete(err);
complete = null;
}
});
if (config.data) {
try {
ps.stdin.end(config.data);
2017-02-05 11:08:26 +01:00
} catch (e) {
logger.error('spawn write error', e);
}
}
process.nextTick(() => {
// it should work without destroy, but a process doesn't get launched
// xubuntu-desktop 19.04 / xfce
// see https://github.com/keeweb/keeweb/issues/1234
ps.stdin.destroy();
});
2017-02-05 11:08:26 +01:00
return ps;
},
checkOpenFiles() {
this.readyToOpenFiles = true;
if (this.pendingFileToOpen) {
this.openFile(this.pendingFileToOpen);
delete this.pendingFileToOpen;
}
},
openFile(file) {
if (this.readyToOpenFiles) {
2019-09-16 22:57:56 +02:00
Events.emit('launcher-open-file', file);
} else {
this.pendingFileToOpen = file;
}
},
setGlobalShortcuts(appSettings) {
this.remoteApp().setGlobalShortcuts(appSettings);
2017-02-05 11:08:26 +01:00
}
};
2019-09-16 22:57:56 +02:00
Events.on('launcher-exit-request', () => {
2017-02-05 11:08:26 +01:00
setTimeout(() => Launcher.exit(), 0);
});
2019-09-16 22:57:56 +02:00
Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0));
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
window.launcherOpen = file => Launcher.openFile(file);
2017-02-05 11:08:26 +01:00
if (window.launcherOpenedFile) {
logger.info('Open file request', window.launcherOpenedFile);
Launcher.openFile(window.launcherOpenedFile);
2017-02-05 11:08:26 +01:00
delete window.launcherOpenedFile;
2015-10-22 20:03:44 +02:00
}
2019-09-16 22:57:56 +02:00
Events.on('app-ready', () => setTimeout(() => Launcher.checkOpenFiles(), 0));
2015-10-22 20:03:44 +02:00
2019-09-21 07:39:03 +02:00
global.Events = Events;
2019-09-15 14:16:32 +02:00
export { Launcher };