keeweb/desktop/app.js

410 lines
13 KiB
JavaScript

const electron = require('electron');
const app = electron.app;
const path = require('path');
const fs = require('fs');
let mainWindow = null;
let appIcon = null;
let openFile = process.argv.filter(arg => /\.kdbx$/i.test(arg))[0];
let ready = false;
let appReady = false;
let restartPending = false;
let mainWindowPosition = {};
let updateMainWindowPositionTimeout = null;
const userDataDir = app.getPath('userData').replace(/[\\/]temp[\\/]\d+\.\d+[\\/]?$/, '');
const windowPositionFileName = path.join(userDataDir, 'window-position.json');
const appSettingsFileName = path.join(userDataDir, 'app-settings.json');
const tempUserDataPath = path.join(userDataDir, 'temp');
const tempUserDataPathRand = Date.now().toString() + Math.random().toString();
const systemNotificationIds = [];
let htmlPath = process.argv.filter(arg => arg.startsWith('--htmlpath=')).map(arg => arg.replace('--htmlpath=', ''))[0];
if (!htmlPath) {
htmlPath = 'file://' + path.join(__dirname, 'index.html');
}
const showDevToolsOnStart = process.argv.some(arg => arg.startsWith('--devtools'));
app.setPath('userData', path.join(tempUserDataPath, tempUserDataPathRand));
setEnv();
app.on('window-all-closed', () => {
if (restartPending) {
// unbind all handlers, load new app.js module and pass control to it
app.removeAllListeners('window-all-closed');
app.removeAllListeners('ready');
app.removeAllListeners('open-file');
app.removeAllListeners('activate');
electron.globalShortcut.unregisterAll();
electron.powerMonitor.removeAllListeners('suspend');
electron.powerMonitor.removeAllListeners('resume');
for (const id of systemNotificationIds) {
electron.systemPreferences.unsubscribeNotification(id);
}
systemNotificationIds.length = 0;
const userDataAppFile = path.join(userDataDir, 'app.asar/app.js');
delete require.cache[require.resolve('./app.js')];
require(userDataAppFile);
app.emit('ready');
} else {
if (process.platform !== 'darwin') {
app.quit();
}
}
});
app.on('ready', () => {
if (!checkSingleInstance()) {
appReady = true;
setAppOptions();
createMainWindow();
setGlobalShortcuts();
subscribePowerEvents();
deleteOldTempFiles();
}
});
app.on('open-file', (e, path) => {
e.preventDefault();
openFile = path;
notifyOpenFile();
});
app.on('activate', () => {
if (process.platform === 'darwin') {
if (appReady && !mainWindow) {
createMainWindow();
}
}
});
app.on('will-quit', () => {
electron.globalShortcut.unregisterAll();
});
app.restartApp = function () {
restartPending = true;
mainWindow.close();
setTimeout(() => {
restartPending = false;
}, 1000);
};
app.openWindow = function (opts) {
return new electron.BrowserWindow(opts);
};
app.minimizeApp = function () {
if (process.platform !== 'darwin') {
mainWindow.minimize();
mainWindow.setSkipTaskbar(true);
appIcon = new electron.Tray(path.join(__dirname, 'icon.png'));
appIcon.on('click', restoreMainWindow);
const contextMenu = electron.Menu.buildFromTemplate([
{label: 'Open KeeWeb', click: restoreMainWindow},
{label: 'Quit KeeWeb', click: closeMainWindow}
]);
appIcon.setContextMenu(contextMenu);
appIcon.setToolTip('KeeWeb');
}
};
app.getMainWindow = function () {
return mainWindow;
};
app.emitBackboneEvent = emitBackboneEvent;
function checkSingleInstance() {
const shouldQuit = app.makeSingleInstance((/* commandLine, workingDirectory */) => {
restoreMainWindow();
});
if (shouldQuit) {
app.quit();
}
return shouldQuit;
}
function setAppOptions() {
app.commandLine.appendSwitch('disable-background-timer-throttling');
}
function readAppSettings() {
try {
return JSON.parse(fs.readFileSync(appSettingsFileName, 'utf8'));
} catch (e) {
return null;
}
}
function createMainWindow() {
const appSettings = readAppSettings();
mainWindow = new electron.BrowserWindow({
show: false,
width: 1000, height: 700, minWidth: 700, minHeight: 400,
icon: path.join(__dirname, 'icon.png'),
titleBarStyle: appSettings ? appSettings.titlebarStyle : undefined,
webPreferences: {
backgroundThrottling: false
}
});
setMenu();
mainWindow.loadURL(htmlPath);
if (showDevToolsOnStart) {
mainWindow.openDevTools();
}
mainWindow.webContents.on('dom-ready', () => {
setTimeout(() => {
mainWindow.show();
ready = true;
notifyOpenFile();
}, 50);
});
mainWindow.webContents.on('context-menu', onContextMenu);
mainWindow.on('resize', delaySaveMainWindowPosition);
mainWindow.on('move', delaySaveMainWindowPosition);
mainWindow.on('close', updateMainWindowPositionIfPending);
mainWindow.on('blur', mainWindowBlur);
mainWindow.on('closed', () => {
mainWindow = null;
saveMainWindowPosition();
});
mainWindow.on('minimize', () => {
emitBackboneEvent('launcher-minimize');
});
mainWindow.on('leave-full-screen', () => {
emitBackboneEvent('leave-full-screen');
});
mainWindow.on('enter-full-screen', () => {
emitBackboneEvent('enter-full-screen');
});
mainWindow.on('session-end', () => {
emitBackboneEvent('os-lock');
});
restoreMainWindowPosition();
}
function restoreMainWindow() {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.setSkipTaskbar(false);
mainWindow.focus();
setTimeout(destroyAppIcon, 0);
}
function closeMainWindow() {
emitBackboneEvent('launcher-exit-request');
setTimeout(destroyAppIcon, 0);
}
function destroyAppIcon() {
if (appIcon) {
appIcon.destroy();
appIcon = null;
}
}
function delaySaveMainWindowPosition() {
if (updateMainWindowPositionTimeout) {
clearTimeout(updateMainWindowPositionTimeout);
}
updateMainWindowPositionTimeout = setTimeout(updateMainWindowPosition, 500);
}
function updateMainWindowPositionIfPending() {
if (updateMainWindowPositionTimeout) {
clearTimeout(updateMainWindowPositionTimeout);
updateMainWindowPosition();
}
}
function updateMainWindowPosition() {
if (!mainWindow) {
return;
}
updateMainWindowPositionTimeout = null;
const bounds = mainWindow.getBounds();
if (!mainWindow.isMaximized() && !mainWindow.isMinimized() && !mainWindow.isFullScreen()) {
mainWindowPosition.x = bounds.x;
mainWindowPosition.y = bounds.y;
mainWindowPosition.width = bounds.width;
mainWindowPosition.height = bounds.height;
}
mainWindowPosition.maximized = mainWindow.isMaximized();
mainWindowPosition.fullScreen = mainWindow.isFullScreen();
mainWindowPosition.displayBounds = require('electron').screen.getDisplayMatching(bounds).bounds;
mainWindowPosition.changed = true;
}
function saveMainWindowPosition() {
if (!mainWindowPosition.changed) {
return;
}
delete mainWindowPosition.changed;
try {
fs.writeFileSync(windowPositionFileName, JSON.stringify(mainWindowPosition), 'utf8');
} catch (e) {}
}
function restoreMainWindowPosition() {
fs.readFile(windowPositionFileName, 'utf8', (e, data) => {
if (data) {
mainWindowPosition = JSON.parse(data);
if (mainWindow && mainWindowPosition) {
if (mainWindowPosition.width && mainWindowPosition.height) {
const displayBounds = require('electron').screen.getDisplayMatching(mainWindowPosition).bounds;
const db = mainWindowPosition.displayBounds;
if (displayBounds.x === db.x && displayBounds.y === db.y &&
displayBounds.width === db.width && displayBounds.height === db.height) {
mainWindow.setBounds(mainWindowPosition);
}
}
if (mainWindowPosition.maximized) { mainWindow.maximize(); }
if (mainWindowPosition.fullScreen) { mainWindow.setFullScreen(true); }
}
}
});
}
function mainWindowBlur() {
emitBackboneEvent('main-window-blur');
}
function emitBackboneEvent(e, arg) {
if (mainWindow && mainWindow.webContents) {
arg = JSON.stringify(arg);
mainWindow.webContents.executeJavaScript(`Backbone.trigger('${e}', ${arg}); void 0;`);
}
}
function setMenu() {
if (process.platform === 'darwin') {
const name = require('electron').app.getName();
const template = [
{
label: name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services', submenu: [] },
{ type: 'separator' },
{ accelerator: 'Command+H', role: 'hide' },
{ accelerator: 'Command+Shift+H', role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit', accelerator: 'Command+Q' }
]
},
{
label: 'Edit',
submenu: [
{ accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' },
{ type: 'separator' },
{ accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ accelerator: 'CmdOrCtrl+A', role: 'selectall' }
]
},
{
label: 'Window',
submenu: [
{ accelerator: 'CmdOrCtrl+M', role: 'minimize' }
]
}
];
const menu = electron.Menu.buildFromTemplate(template);
electron.Menu.setApplicationMenu(menu);
}
}
function onContextMenu(e, props) {
if (props.inputFieldType !== 'plainText' || !props.isEditable) {
return;
}
const Menu = electron.Menu;
const inputMenu = Menu.buildFromTemplate([
{role: 'undo'},
{role: 'redo'},
{type: 'separator'},
{role: 'cut'},
{role: 'copy'},
{role: 'paste'},
{type: 'separator'},
{role: 'selectall'}
]);
inputMenu.popup(mainWindow);
}
function notifyOpenFile() {
if (ready && openFile && mainWindow) {
openFile = openFile.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
mainWindow.webContents.executeJavaScript('if (window.launcherOpen) { window.launcherOpen("' + openFile + '"); } ' +
' else { window.launcherOpenedFile="' + openFile + '"; }');
openFile = null;
}
}
function setGlobalShortcuts() {
const shortcutModifiers = process.platform === 'darwin' ? 'Ctrl+Alt+' : 'Shift+Alt+';
const shortcuts = {
C: 'copy-password',
B: 'copy-user',
U: 'copy-url',
T: 'auto-type'
};
Object.keys(shortcuts).forEach(key => {
const shortcut = shortcutModifiers + key;
const eventName = shortcuts[key];
try {
electron.globalShortcut.register(shortcut, () => {
emitBackboneEvent(eventName);
});
} catch (e) {}
});
}
function subscribePowerEvents() {
electron.powerMonitor.on('suspend', () => {
emitBackboneEvent('power-monitor-suspend');
});
electron.powerMonitor.on('resume', () => {
emitBackboneEvent('power-monitor-resume');
});
if (process.platform === 'darwin') {
const id = electron.systemPreferences.subscribeNotification('com.apple.screenIsLocked', () => {
emitBackboneEvent('os-lock');
});
systemNotificationIds.push(id);
}
}
function setEnv() {
if (process.platform === 'linux' && ['Pantheon', 'Unity:Unity7'].indexOf(process.env.XDG_CURRENT_DESKTOP) !== -1) {
// https://github.com/electron/electron/issues/9046
process.env.XDG_CURRENT_DESKTOP = 'Unity';
}
}
function deleteOldTempFiles() {
if (app.oldTempFilesDeleted) {
return;
}
setTimeout(() => {
for (const dir of fs.readdirSync(tempUserDataPath)) {
if (dir !== tempUserDataPathRand) {
try {
deleteRecursive(path.join(tempUserDataPath, dir));
} catch (e) {}
}
}
app.oldTempFilesDeleted = true; // this is added to prevent file deletion on restart
}, 1000);
}
function deleteRecursive(dir) {
for (const file of fs.readdirSync(dir)) {
const filePath = path.join(dir, file);
if (fs.lstatSync(filePath).isDirectory()) {
deleteRecursive(filePath);
} else {
fs.unlinkSync(filePath);
}
}
fs.rmdirSync(dir);
}