mirror of
https://github.com/bitwarden/jslib
synced 2025-12-06 00:03:29 +00:00
Apply Prettier (#581)
This commit is contained in:
@@ -1,229 +1,224 @@
|
||||
import {
|
||||
app,
|
||||
clipboard,
|
||||
dialog,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
} from 'electron';
|
||||
import { app, clipboard, dialog, Menu, MenuItemConstructorOptions } from "electron";
|
||||
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { WindowMain } from './window.main';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { WindowMain } from "./window.main";
|
||||
|
||||
export class BaseMenu {
|
||||
protected editMenuItemOptions: MenuItemConstructorOptions;
|
||||
protected viewSubMenuItemOptions: MenuItemConstructorOptions[];
|
||||
protected windowMenuItemOptions: MenuItemConstructorOptions;
|
||||
protected macAppMenuItemOptions: MenuItemConstructorOptions[];
|
||||
protected macWindowSubmenuOptions: MenuItemConstructorOptions[];
|
||||
protected editMenuItemOptions: MenuItemConstructorOptions;
|
||||
protected viewSubMenuItemOptions: MenuItemConstructorOptions[];
|
||||
protected windowMenuItemOptions: MenuItemConstructorOptions;
|
||||
protected macAppMenuItemOptions: MenuItemConstructorOptions[];
|
||||
protected macWindowSubmenuOptions: MenuItemConstructorOptions[];
|
||||
|
||||
constructor(protected i18nService: I18nService, protected windowMain: WindowMain) { }
|
||||
constructor(protected i18nService: I18nService, protected windowMain: WindowMain) {}
|
||||
|
||||
protected initProperties() {
|
||||
this.editMenuItemOptions = {
|
||||
label: this.i18nService.t('edit'),
|
||||
submenu: [
|
||||
{
|
||||
label: this.i18nService.t('undo'),
|
||||
role: 'undo',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('redo'),
|
||||
role: 'redo',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('cut'),
|
||||
role: 'cut',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('copy'),
|
||||
role: 'copy',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('paste'),
|
||||
role: 'paste',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('selectAll'),
|
||||
role: 'selectAll',
|
||||
},
|
||||
],
|
||||
};
|
||||
protected initProperties() {
|
||||
this.editMenuItemOptions = {
|
||||
label: this.i18nService.t("edit"),
|
||||
submenu: [
|
||||
{
|
||||
label: this.i18nService.t("undo"),
|
||||
role: "undo",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("redo"),
|
||||
role: "redo",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("cut"),
|
||||
role: "cut",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("copy"),
|
||||
role: "copy",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("paste"),
|
||||
role: "paste",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("selectAll"),
|
||||
role: "selectAll",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
this.viewSubMenuItemOptions = [
|
||||
{
|
||||
label: this.i18nService.t('zoomIn'),
|
||||
role: 'zoomIn',
|
||||
accelerator: 'CmdOrCtrl+=',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('zoomOut'),
|
||||
role: 'zoomOut',
|
||||
accelerator: 'CmdOrCtrl+-',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('resetZoom'),
|
||||
role: 'resetZoom',
|
||||
accelerator: 'CmdOrCtrl+0',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('toggleFullScreen'),
|
||||
role: 'togglefullscreen',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('reload'),
|
||||
role: 'forceReload',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('toggleDevTools'),
|
||||
role: 'toggleDevTools',
|
||||
accelerator: 'F12',
|
||||
},
|
||||
];
|
||||
this.viewSubMenuItemOptions = [
|
||||
{
|
||||
label: this.i18nService.t("zoomIn"),
|
||||
role: "zoomIn",
|
||||
accelerator: "CmdOrCtrl+=",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("zoomOut"),
|
||||
role: "zoomOut",
|
||||
accelerator: "CmdOrCtrl+-",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("resetZoom"),
|
||||
role: "resetZoom",
|
||||
accelerator: "CmdOrCtrl+0",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("toggleFullScreen"),
|
||||
role: "togglefullscreen",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("reload"),
|
||||
role: "forceReload",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("toggleDevTools"),
|
||||
role: "toggleDevTools",
|
||||
accelerator: "F12",
|
||||
},
|
||||
];
|
||||
|
||||
this.windowMenuItemOptions = {
|
||||
label: this.i18nService.t('window'),
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{
|
||||
label: this.i18nService.t('minimize'),
|
||||
role: 'minimize',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('close'),
|
||||
role: 'close',
|
||||
},
|
||||
],
|
||||
};
|
||||
this.windowMenuItemOptions = {
|
||||
label: this.i18nService.t("window"),
|
||||
role: "window",
|
||||
submenu: [
|
||||
{
|
||||
label: this.i18nService.t("minimize"),
|
||||
role: "minimize",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("close"),
|
||||
role: "close",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
this.macAppMenuItemOptions = [
|
||||
{
|
||||
label: this.i18nService.t('services'),
|
||||
role: 'services', submenu: [],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('hideBitwarden'),
|
||||
role: 'hide',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('hideOthers'),
|
||||
role: 'hideOthers',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('showAll'),
|
||||
role: 'unhide',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('quitBitwarden'),
|
||||
role: 'quit',
|
||||
},
|
||||
];
|
||||
if (process.platform === "darwin") {
|
||||
this.macAppMenuItemOptions = [
|
||||
{
|
||||
label: this.i18nService.t("services"),
|
||||
role: "services",
|
||||
submenu: [],
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("hideBitwarden"),
|
||||
role: "hide",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("hideOthers"),
|
||||
role: "hideOthers",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("showAll"),
|
||||
role: "unhide",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("quitBitwarden"),
|
||||
role: "quit",
|
||||
},
|
||||
];
|
||||
|
||||
this.macWindowSubmenuOptions = [
|
||||
{
|
||||
label: this.i18nService.t('minimize'),
|
||||
role: 'minimize',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('zoom'),
|
||||
role: 'zoom',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('bringAllToFront'),
|
||||
role: 'front',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('close'),
|
||||
role: 'close',
|
||||
},
|
||||
];
|
||||
}
|
||||
this.macWindowSubmenuOptions = [
|
||||
{
|
||||
label: this.i18nService.t("minimize"),
|
||||
role: "minimize",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("zoom"),
|
||||
role: "zoom",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("bringAllToFront"),
|
||||
role: "front",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("close"),
|
||||
role: "close",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected initContextMenu() {
|
||||
if (this.windowMain.win == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected initContextMenu() {
|
||||
if (this.windowMain.win == null) {
|
||||
return;
|
||||
}
|
||||
const selectionMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: this.i18nService.t("copy"),
|
||||
role: "copy",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("selectAll"),
|
||||
role: "selectAll",
|
||||
},
|
||||
]);
|
||||
|
||||
const selectionMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: this.i18nService.t('copy'),
|
||||
role: 'copy',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('selectAll'),
|
||||
role: 'selectAll',
|
||||
},
|
||||
]);
|
||||
const inputMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: this.i18nService.t("undo"),
|
||||
role: "undo",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("redo"),
|
||||
role: "redo",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("cut"),
|
||||
role: "cut",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("copy"),
|
||||
role: "copy",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("paste"),
|
||||
role: "paste",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("selectAll"),
|
||||
role: "selectAll",
|
||||
},
|
||||
]);
|
||||
|
||||
const inputMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: this.i18nService.t('undo'),
|
||||
role: 'undo',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('redo'),
|
||||
role: 'redo',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('cut'),
|
||||
role: 'cut',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('copy'),
|
||||
role: 'copy',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('paste'),
|
||||
role: 'paste',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('selectAll'),
|
||||
role: 'selectAll',
|
||||
},
|
||||
]);
|
||||
const inputSelectionMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: this.i18nService.t("cut"),
|
||||
role: "cut",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("copy"),
|
||||
role: "copy",
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("paste"),
|
||||
role: "paste",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("selectAll"),
|
||||
role: "selectAll",
|
||||
},
|
||||
]);
|
||||
|
||||
const inputSelectionMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: this.i18nService.t('cut'),
|
||||
role: 'cut',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('copy'),
|
||||
role: 'copy',
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t('paste'),
|
||||
role: 'paste',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('selectAll'),
|
||||
role: 'selectAll',
|
||||
},
|
||||
]);
|
||||
|
||||
this.windowMain.win.webContents.on('context-menu', (e, props) => {
|
||||
const selected = props.selectionText && props.selectionText.trim() !== '';
|
||||
if (props.isEditable && selected) {
|
||||
inputSelectionMenu.popup({ window: this.windowMain.win });
|
||||
} else if (props.isEditable) {
|
||||
inputMenu.popup({ window: this.windowMain.win });
|
||||
} else if (selected) {
|
||||
selectionMenu.popup({ window: this.windowMain.win });
|
||||
}
|
||||
});
|
||||
}
|
||||
this.windowMain.win.webContents.on("context-menu", (e, props) => {
|
||||
const selected = props.selectionText && props.selectionText.trim() !== "";
|
||||
if (props.isEditable && selected) {
|
||||
inputSelectionMenu.popup({ window: this.windowMain.win });
|
||||
} else if (props.isEditable) {
|
||||
inputMenu.popup({ window: this.windowMain.win });
|
||||
} else if (selected) {
|
||||
selectionMenu.popup({ window: this.windowMain.win });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { ipcMain, systemPreferences } from 'electron';
|
||||
import { ipcMain, systemPreferences } from "electron";
|
||||
|
||||
import { BiometricMain } from 'jslib-common/abstractions/biometric.main';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||
import { BiometricMain } from "jslib-common/abstractions/biometric.main";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
export default class BiometricDarwinMain implements BiometricMain {
|
||||
isError: boolean = false;
|
||||
isError: boolean = false;
|
||||
|
||||
constructor(private i18nservice: I18nService, private stateService: StateService) {}
|
||||
constructor(private i18nservice: I18nService, private stateService: StateService) {}
|
||||
|
||||
async init() {
|
||||
await this.stateService.setEnableBiometric(await this.supportsBiometric());
|
||||
await this.stateService.setBiometricText('unlockWithTouchId');
|
||||
await this.stateService.setNoAutoPromptBiometricsText('noAutoPromptTouchId');
|
||||
async init() {
|
||||
await this.stateService.setEnableBiometric(await this.supportsBiometric());
|
||||
await this.stateService.setBiometricText("unlockWithTouchId");
|
||||
await this.stateService.setNoAutoPromptBiometricsText("noAutoPromptTouchId");
|
||||
|
||||
ipcMain.on('biometric', async (event: any, message: any) => {
|
||||
event.returnValue = await this.authenticateBiometric();
|
||||
});
|
||||
}
|
||||
|
||||
supportsBiometric(): Promise<boolean> {
|
||||
return Promise.resolve(systemPreferences.canPromptTouchID());
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
try {
|
||||
await systemPreferences.promptTouchID(this.i18nservice.t('touchIdConsentMessage'));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
ipcMain.on("biometric", async (event: any, message: any) => {
|
||||
event.returnValue = await this.authenticateBiometric();
|
||||
});
|
||||
}
|
||||
|
||||
supportsBiometric(): Promise<boolean> {
|
||||
return Promise.resolve(systemPreferences.canPromptTouchID());
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
try {
|
||||
await systemPreferences.promptTouchID(this.i18nservice.t("touchIdConsentMessage"));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +1,144 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import forceFocus from 'forcefocus';
|
||||
import { ipcMain } from "electron";
|
||||
import forceFocus from "forcefocus";
|
||||
|
||||
import { WindowMain } from './window.main';
|
||||
import { WindowMain } from "./window.main";
|
||||
|
||||
import { BiometricMain } from 'jslib-common/abstractions/biometric.main';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||
import { BiometricMain } from "jslib-common/abstractions/biometric.main";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
export default class BiometricWindowsMain implements BiometricMain {
|
||||
isError: boolean = false;
|
||||
isError: boolean = false;
|
||||
|
||||
private windowsSecurityCredentialsUiModule: any;
|
||||
private windowsSecurityCredentialsUiModule: any;
|
||||
|
||||
constructor(private i18nservice: I18nService, private windowMain: WindowMain,
|
||||
private stateService: StateService, private logService: LogService) { }
|
||||
constructor(
|
||||
private i18nservice: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private stateService: StateService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule();
|
||||
let supportsBiometric = false;
|
||||
async init() {
|
||||
this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule();
|
||||
let supportsBiometric = false;
|
||||
try {
|
||||
supportsBiometric = await this.supportsBiometric();
|
||||
} catch {
|
||||
// store error state so we can let the user know on the settings page
|
||||
this.isError = true;
|
||||
}
|
||||
await this.stateService.setEnableBiometric(supportsBiometric);
|
||||
await this.stateService.setBiometricText("unlockWithWindowsHello");
|
||||
await this.stateService.setNoAutoPromptBiometricsText("noAutoPromptWindowsHello");
|
||||
|
||||
ipcMain.on("biometric", async (event: any, message: any) => {
|
||||
event.returnValue = await this.authenticateBiometric();
|
||||
});
|
||||
}
|
||||
|
||||
async supportsBiometric(): Promise<boolean> {
|
||||
const availability = await this.checkAvailabilityAsync();
|
||||
|
||||
return this.getAllowedAvailabilities().includes(availability);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const verification = await this.requestVerificationAsync(
|
||||
this.i18nservice.t("windowsHelloConsentMessage")
|
||||
);
|
||||
|
||||
return verification === module.UserConsentVerificationResult.verified;
|
||||
}
|
||||
|
||||
getWindowsSecurityCredentialsUiModule(): any {
|
||||
try {
|
||||
if (this.windowsSecurityCredentialsUiModule == null && this.getWindowsMajorVersion() >= 10) {
|
||||
this.windowsSecurityCredentialsUiModule = require("@nodert-win10-rs4/windows.security.credentials.ui");
|
||||
}
|
||||
return this.windowsSecurityCredentialsUiModule;
|
||||
} catch {
|
||||
this.isError = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async checkAvailabilityAsync(): Promise<any> {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module != null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
supportsBiometric = await this.supportsBiometric();
|
||||
} catch {
|
||||
// store error state so we can let the user know on the settings page
|
||||
this.isError = true;
|
||||
}
|
||||
await this.stateService.setEnableBiometric(supportsBiometric);
|
||||
await this.stateService.setBiometricText('unlockWithWindowsHello');
|
||||
await this.stateService.setNoAutoPromptBiometricsText('noAutoPromptWindowsHello');
|
||||
|
||||
ipcMain.on('biometric', async (event: any, message: any) => {
|
||||
event.returnValue = await this.authenticateBiometric();
|
||||
});
|
||||
}
|
||||
|
||||
async supportsBiometric(): Promise<boolean> {
|
||||
const availability = await this.checkAvailabilityAsync();
|
||||
|
||||
return this.getAllowedAvailabilities().includes(availability);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const verification = await this.requestVerificationAsync(this.i18nservice.t('windowsHelloConsentMessage'));
|
||||
|
||||
return verification === module.UserConsentVerificationResult.verified;
|
||||
}
|
||||
|
||||
getWindowsSecurityCredentialsUiModule(): any {
|
||||
try {
|
||||
if (this.windowsSecurityCredentialsUiModule == null && this.getWindowsMajorVersion() >= 10) {
|
||||
this.windowsSecurityCredentialsUiModule = require('@nodert-win10-rs4/windows.security.credentials.ui');
|
||||
module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => {
|
||||
if (error) {
|
||||
return resolve(null);
|
||||
}
|
||||
return this.windowsSecurityCredentialsUiModule;
|
||||
return resolve(result);
|
||||
});
|
||||
} catch {
|
||||
this.isError = true;
|
||||
this.isError = true;
|
||||
return resolve(null);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
async checkAvailabilityAsync(): Promise<any> {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module != null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => {
|
||||
if (error) {
|
||||
return resolve(null);
|
||||
}
|
||||
return resolve(result);
|
||||
});
|
||||
} catch {
|
||||
this.isError = true;
|
||||
return resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
async requestVerificationAsync(message: string): Promise<any> {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module != null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
module.UserConsentVerifier.requestVerificationAsync(message, (error: Error, result: any) => {
|
||||
if (error) {
|
||||
return resolve(null);
|
||||
}
|
||||
return resolve(result);
|
||||
});
|
||||
|
||||
forceFocus.focusWindow(this.windowMain.win);
|
||||
} catch (error) {
|
||||
this.isError = true;
|
||||
return reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
getAllowedAvailabilities(): any[] {
|
||||
async requestVerificationAsync(message: string): Promise<any> {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module != null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module != null) {
|
||||
return [
|
||||
module.UserConsentVerifierAvailability.available,
|
||||
module.UserConsentVerifierAvailability.deviceBusy,
|
||||
];
|
||||
module.UserConsentVerifier.requestVerificationAsync(
|
||||
message,
|
||||
(error: Error, result: any) => {
|
||||
if (error) {
|
||||
return resolve(null);
|
||||
}
|
||||
return resolve(result);
|
||||
}
|
||||
} catch { /*Ignore error*/ }
|
||||
return [];
|
||||
}
|
||||
);
|
||||
|
||||
getWindowsMajorVersion(): number {
|
||||
if (process.platform !== 'win32') {
|
||||
return -1;
|
||||
forceFocus.focusWindow(this.windowMain.win);
|
||||
} catch (error) {
|
||||
this.isError = true;
|
||||
return reject(error);
|
||||
}
|
||||
try {
|
||||
const version = require('os').release();
|
||||
return Number.parseInt(version.split('.')[0], 10);
|
||||
} catch {
|
||||
this.logService.error('Unable to resolve windows major version number');
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
getAllowedAvailabilities(): any[] {
|
||||
try {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module != null) {
|
||||
return [
|
||||
module.UserConsentVerifierAvailability.available,
|
||||
module.UserConsentVerifierAvailability.deviceBusy,
|
||||
];
|
||||
}
|
||||
} catch {
|
||||
/*Ignore error*/
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getWindowsMajorVersion(): number {
|
||||
if (process.platform !== "win32") {
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
const version = require("os").release();
|
||||
return Number.parseInt(version.split(".")[0], 10);
|
||||
} catch {
|
||||
this.logService.error("Unable to resolve windows major version number");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
2
electron/src/globals.d.ts
vendored
2
electron/src/globals.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module 'forcefocus';
|
||||
declare module "forcefocus";
|
||||
|
||||
@@ -1,56 +1,52 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import {
|
||||
deletePassword,
|
||||
getPassword,
|
||||
setPassword,
|
||||
} from 'keytar';
|
||||
import { deletePassword, getPassword, setPassword } from "keytar";
|
||||
|
||||
import { BiometricMain } from 'jslib-common/abstractions/biometric.main';
|
||||
import { BiometricMain } from "jslib-common/abstractions/biometric.main";
|
||||
|
||||
const AuthRequiredSuffix = '_biometric';
|
||||
const AuthenticatedActions = ['getPassword'];
|
||||
const AuthRequiredSuffix = "_biometric";
|
||||
const AuthenticatedActions = ["getPassword"];
|
||||
|
||||
export class KeytarStorageListener {
|
||||
constructor(private serviceName: string, private biometricService: BiometricMain) { }
|
||||
constructor(private serviceName: string, private biometricService: BiometricMain) {}
|
||||
|
||||
init() {
|
||||
ipcMain.on('keytar', async (event: any, message: any) => {
|
||||
try {
|
||||
let serviceName = this.serviceName;
|
||||
message.keySuffix = '_' + (message.keySuffix ?? '');
|
||||
if (message.keySuffix !== '_') {
|
||||
serviceName += message.keySuffix;
|
||||
}
|
||||
|
||||
const authenticationRequired = AuthenticatedActions.includes(message.action) &&
|
||||
AuthRequiredSuffix === message.keySuffix;
|
||||
const authenticated = !authenticationRequired || await this.authenticateBiometric();
|
||||
|
||||
let val: string | boolean = null;
|
||||
if (authenticated && message.action && message.key) {
|
||||
if (message.action === 'getPassword') {
|
||||
val = await getPassword(serviceName, message.key);
|
||||
} else if (message.action === 'hasPassword') {
|
||||
const result = await getPassword(serviceName, message.key);
|
||||
val = result != null;
|
||||
} else if (message.action === 'setPassword' && message.value) {
|
||||
await setPassword(serviceName, message.key, message.value);
|
||||
} else if (message.action === 'deletePassword') {
|
||||
await deletePassword(serviceName, message.key);
|
||||
}
|
||||
}
|
||||
event.returnValue = val;
|
||||
} catch {
|
||||
event.returnValue = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async authenticateBiometric(): Promise<boolean> {
|
||||
if (this.biometricService) {
|
||||
return await this.biometricService.authenticateBiometric();
|
||||
init() {
|
||||
ipcMain.on("keytar", async (event: any, message: any) => {
|
||||
try {
|
||||
let serviceName = this.serviceName;
|
||||
message.keySuffix = "_" + (message.keySuffix ?? "");
|
||||
if (message.keySuffix !== "_") {
|
||||
serviceName += message.keySuffix;
|
||||
}
|
||||
return false;
|
||||
|
||||
const authenticationRequired =
|
||||
AuthenticatedActions.includes(message.action) && AuthRequiredSuffix === message.keySuffix;
|
||||
const authenticated = !authenticationRequired || (await this.authenticateBiometric());
|
||||
|
||||
let val: string | boolean = null;
|
||||
if (authenticated && message.action && message.key) {
|
||||
if (message.action === "getPassword") {
|
||||
val = await getPassword(serviceName, message.key);
|
||||
} else if (message.action === "hasPassword") {
|
||||
const result = await getPassword(serviceName, message.key);
|
||||
val = result != null;
|
||||
} else if (message.action === "setPassword" && message.value) {
|
||||
await setPassword(serviceName, message.key, message.value);
|
||||
} else if (message.action === "deletePassword") {
|
||||
await deletePassword(serviceName, message.key);
|
||||
}
|
||||
}
|
||||
event.returnValue = val;
|
||||
} catch {
|
||||
event.returnValue = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async authenticateBiometric(): Promise<boolean> {
|
||||
if (this.biometricService) {
|
||||
return await this.biometricService.authenticateBiometric();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,74 @@
|
||||
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
import { CryptoService } from 'jslib-common/services/crypto.service';
|
||||
import { CryptoService } from "jslib-common/services/crypto.service";
|
||||
|
||||
import { KeySuffixOptions } from 'jslib-common/enums/keySuffixOptions';
|
||||
import { StorageLocation } from 'jslib-common/enums/storageLocation';
|
||||
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
|
||||
import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions";
|
||||
import { StorageLocation } from "jslib-common/enums/storageLocation";
|
||||
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||
|
||||
export class ElectronCryptoService extends CryptoService {
|
||||
constructor(
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
platformUtilService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
stateService: StateService
|
||||
) {
|
||||
super(cryptoFunctionService, platformUtilService, logService, stateService);
|
||||
}
|
||||
|
||||
constructor(cryptoFunctionService: CryptoFunctionService, platformUtilService: PlatformUtilsService,
|
||||
logService: LogService, stateService: StateService) {
|
||||
super(cryptoFunctionService, platformUtilService, logService, stateService);
|
||||
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
|
||||
await this.upgradeSecurelyStoredKey();
|
||||
return super.hasKeyStored(keySuffix);
|
||||
}
|
||||
|
||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
this.clearStoredKey(KeySuffixOptions.Auto);
|
||||
}
|
||||
|
||||
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
|
||||
await this.upgradeSecurelyStoredKey();
|
||||
return super.hasKeyStored(keySuffix);
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
this.clearStoredKey(KeySuffixOptions.Biometric);
|
||||
}
|
||||
}
|
||||
|
||||
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) {
|
||||
await this.upgradeSecurelyStoredKey();
|
||||
return super.retrieveKeyFromStorage(keySuffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4 Jun 2021 This is temporary upgrade method to move from a single shared stored key to
|
||||
* multiple, unique stored keys for each use, e.g. never logout vs. biometric authentication.
|
||||
*/
|
||||
private async upgradeSecurelyStoredKey() {
|
||||
// attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway.
|
||||
const key = await this.stateService.getCryptoMasterKeyB64();
|
||||
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
this.clearStoredKey(KeySuffixOptions.Auto);
|
||||
}
|
||||
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
this.clearStoredKey(KeySuffixOptions.Biometric);
|
||||
}
|
||||
try {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key);
|
||||
}
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Biometric)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(
|
||||
`Encountered error while upgrading obsolete Bitwarden secure storage item:`
|
||||
);
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) {
|
||||
await this.upgradeSecurelyStoredKey();
|
||||
return super.retrieveKeyFromStorage(keySuffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4 Jun 2021 This is temporary upgrade method to move from a single shared stored key to
|
||||
* multiple, unique stored keys for each use, e.g. never logout vs. biometric authentication.
|
||||
*/
|
||||
private async upgradeSecurelyStoredKey() {
|
||||
// attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway.
|
||||
const key = await this.stateService.getCryptoMasterKeyB64();
|
||||
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key);
|
||||
}
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Biometric)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(`Encountered error while upgrading obsolete Bitwarden secure storage item:`);
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
await this.stateService.setCryptoMasterKeyB64(null);
|
||||
}
|
||||
await this.stateService.setCryptoMasterKeyB64(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
import log from 'electron-log';
|
||||
import * as path from 'path';
|
||||
import log from "electron-log";
|
||||
import * as path from "path";
|
||||
|
||||
import { isDev } from '../utils';
|
||||
import { isDev } from "../utils";
|
||||
|
||||
import { LogLevelType } from 'jslib-common/enums/logLevelType';
|
||||
import { LogLevelType } from "jslib-common/enums/logLevelType";
|
||||
|
||||
import { ConsoleLogService as BaseLogService } from 'jslib-common/services/consoleLog.service';
|
||||
import { ConsoleLogService as BaseLogService } from "jslib-common/services/consoleLog.service";
|
||||
|
||||
export class ElectronLogService extends BaseLogService {
|
||||
|
||||
constructor(protected filter: (level: LogLevelType) => boolean = null, logDir: string = null) {
|
||||
super(isDev(), filter);
|
||||
if (log.transports == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.transports.file.level = 'info';
|
||||
if (logDir != null) {
|
||||
log.transports.file.file = path.join(logDir, 'app.log');
|
||||
}
|
||||
constructor(protected filter: (level: LogLevelType) => boolean = null, logDir: string = null) {
|
||||
super(isDev(), filter);
|
||||
if (log.transports == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
write(level: LogLevelType, message: string) {
|
||||
if (this.filter != null && this.filter(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (level) {
|
||||
case LogLevelType.Debug:
|
||||
log.debug(message);
|
||||
break;
|
||||
case LogLevelType.Info:
|
||||
log.info(message);
|
||||
break;
|
||||
case LogLevelType.Warning:
|
||||
log.warn(message);
|
||||
break;
|
||||
case LogLevelType.Error:
|
||||
log.error(message);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
log.transports.file.level = "info";
|
||||
if (logDir != null) {
|
||||
log.transports.file.file = path.join(logDir, "app.log");
|
||||
}
|
||||
}
|
||||
|
||||
write(level: LogLevelType, message: string) {
|
||||
if (this.filter != null && this.filter(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (level) {
|
||||
case LogLevelType.Debug:
|
||||
log.debug(message);
|
||||
break;
|
||||
case LogLevelType.Info:
|
||||
log.info(message);
|
||||
break;
|
||||
case LogLevelType.Warning:
|
||||
log.warn(message);
|
||||
break;
|
||||
case LogLevelType.Error:
|
||||
log.error(message);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,66 @@
|
||||
import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme } from 'electron';
|
||||
import { promises as fs } from 'fs';
|
||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
||||
import { RendererMenuItem } from '../utils';
|
||||
import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme } from "electron";
|
||||
import { promises as fs } from "fs";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { RendererMenuItem } from "../utils";
|
||||
|
||||
import { ThemeType } from 'jslib-common/enums/themeType';
|
||||
import { ThemeType } from "jslib-common/enums/themeType";
|
||||
|
||||
import { WindowMain } from '../window.main';
|
||||
import { WindowMain } from "../window.main";
|
||||
|
||||
export class ElectronMainMessagingService implements MessagingService {
|
||||
constructor(private windowMain: WindowMain, private onMessage: (message: any) => void) {
|
||||
ipcMain.handle('appVersion', () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
constructor(private windowMain: WindowMain, private onMessage: (message: any) => void) {
|
||||
ipcMain.handle("appVersion", () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
ipcMain.handle('systemTheme', () => {
|
||||
return nativeTheme.shouldUseDarkColors ? ThemeType.Dark : ThemeType.Light;
|
||||
});
|
||||
ipcMain.handle("systemTheme", () => {
|
||||
return nativeTheme.shouldUseDarkColors ? ThemeType.Dark : ThemeType.Light;
|
||||
});
|
||||
|
||||
ipcMain.handle('showMessageBox', (event, options) => {
|
||||
return dialog.showMessageBox(options);
|
||||
});
|
||||
ipcMain.handle("showMessageBox", (event, options) => {
|
||||
return dialog.showMessageBox(options);
|
||||
});
|
||||
|
||||
ipcMain.handle('openContextMenu', (event, options: {menu: RendererMenuItem[]}) => {
|
||||
return new Promise(resolve => {
|
||||
const menu = new Menu();
|
||||
options.menu.forEach((m, index) => {
|
||||
menu.append(new MenuItem({
|
||||
label: m.label,
|
||||
type: m.type,
|
||||
click: () => {
|
||||
resolve(index);
|
||||
},
|
||||
}));
|
||||
});
|
||||
menu.popup({ window: windowMain.win, callback: () => {
|
||||
resolve(-1);
|
||||
}});
|
||||
});
|
||||
ipcMain.handle("openContextMenu", (event, options: { menu: RendererMenuItem[] }) => {
|
||||
return new Promise((resolve) => {
|
||||
const menu = new Menu();
|
||||
options.menu.forEach((m, index) => {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: m.label,
|
||||
type: m.type,
|
||||
click: () => {
|
||||
resolve(index);
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
menu.popup({
|
||||
window: windowMain.win,
|
||||
callback: () => {
|
||||
resolve(-1);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('windowVisible', () => {
|
||||
return windowMain.win?.isVisible();
|
||||
});
|
||||
ipcMain.handle("windowVisible", () => {
|
||||
return windowMain.win?.isVisible();
|
||||
});
|
||||
|
||||
nativeTheme.on('updated', () => {
|
||||
windowMain.win?.webContents.send('systemThemeUpdated', nativeTheme.shouldUseDarkColors ? ThemeType.Dark : ThemeType.Light);
|
||||
});
|
||||
}
|
||||
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
this.onMessage(message);
|
||||
if (this.windowMain.win != null) {
|
||||
this.windowMain.win.webContents.send('messagingService', message);
|
||||
}
|
||||
nativeTheme.on("updated", () => {
|
||||
windowMain.win?.webContents.send(
|
||||
"systemThemeUpdated",
|
||||
nativeTheme.shouldUseDarkColors ? ThemeType.Dark : ThemeType.Light
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
this.onMessage(message);
|
||||
if (this.windowMain.win != null) {
|
||||
this.windowMain.win.webContents.send("messagingService", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,210 +1,218 @@
|
||||
import {
|
||||
clipboard,
|
||||
ipcRenderer,
|
||||
shell,
|
||||
} from 'electron';
|
||||
import { clipboard, ipcRenderer, shell } from "electron";
|
||||
|
||||
import {
|
||||
isDev,
|
||||
isMacAppStore,
|
||||
} from '../utils';
|
||||
import { isDev, isMacAppStore } from "../utils";
|
||||
|
||||
import { DeviceType } from 'jslib-common/enums/deviceType';
|
||||
import { ThemeType } from 'jslib-common/enums/themeType';
|
||||
import { DeviceType } from "jslib-common/enums/deviceType";
|
||||
import { ThemeType } from "jslib-common/enums/themeType";
|
||||
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
||||
identityClientId: string;
|
||||
identityClientId: string;
|
||||
|
||||
private deviceCache: DeviceType = null;
|
||||
private deviceCache: DeviceType = null;
|
||||
|
||||
constructor(protected i18nService: I18nService, private messagingService: MessagingService,
|
||||
private isDesktopApp: boolean, private stateService: StateService) {
|
||||
this.identityClientId = isDesktopApp ? 'desktop' : 'connector';
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
private messagingService: MessagingService,
|
||||
private isDesktopApp: boolean,
|
||||
private stateService: StateService
|
||||
) {
|
||||
this.identityClientId = isDesktopApp ? "desktop" : "connector";
|
||||
}
|
||||
|
||||
getDevice(): DeviceType {
|
||||
if (!this.deviceCache) {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
this.deviceCache = DeviceType.WindowsDesktop;
|
||||
break;
|
||||
case "darwin":
|
||||
this.deviceCache = DeviceType.MacOsDesktop;
|
||||
break;
|
||||
case "linux":
|
||||
default:
|
||||
this.deviceCache = DeviceType.LinuxDesktop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getDevice(): DeviceType {
|
||||
if (!this.deviceCache) {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
this.deviceCache = DeviceType.WindowsDesktop;
|
||||
break;
|
||||
case 'darwin':
|
||||
this.deviceCache = DeviceType.MacOsDesktop;
|
||||
break;
|
||||
case 'linux':
|
||||
default:
|
||||
this.deviceCache = DeviceType.LinuxDesktop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this.deviceCache;
|
||||
}
|
||||
|
||||
return this.deviceCache;
|
||||
getDeviceString(): string {
|
||||
const device = DeviceType[this.getDevice()].toLowerCase();
|
||||
return device.replace("desktop", "");
|
||||
}
|
||||
|
||||
isFirefox(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isChrome(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isEdge(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isOpera(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isVivaldi(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSafari(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isIE(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMacAppStore(): boolean {
|
||||
return isMacAppStore();
|
||||
}
|
||||
|
||||
isViewOpen(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
launchUri(uri: string, options?: any): void {
|
||||
shell.openExternal(uri);
|
||||
}
|
||||
|
||||
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
|
||||
const blob = new Blob([blobData], blobOptions);
|
||||
const a = win.document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = fileName;
|
||||
win.document.body.appendChild(a);
|
||||
a.click();
|
||||
win.document.body.removeChild(a);
|
||||
}
|
||||
|
||||
getApplicationVersion(): Promise<string> {
|
||||
return ipcRenderer.invoke("appVersion");
|
||||
}
|
||||
|
||||
// Temporarily restricted to only Windows until https://github.com/electron/electron/pull/28349
|
||||
// has been merged and an updated electron build is available.
|
||||
supportsWebAuthn(win: Window): boolean {
|
||||
return process.platform === "win32";
|
||||
}
|
||||
|
||||
supportsDuo(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
showToast(
|
||||
type: "error" | "success" | "warning" | "info",
|
||||
title: string,
|
||||
text: string | string[],
|
||||
options?: any
|
||||
): void {
|
||||
this.messagingService.send("showToast", {
|
||||
text: text,
|
||||
title: title,
|
||||
type: type,
|
||||
options: options,
|
||||
});
|
||||
}
|
||||
|
||||
async showDialog(
|
||||
text: string,
|
||||
title?: string,
|
||||
confirmText?: string,
|
||||
cancelText?: string,
|
||||
type?: string
|
||||
): Promise<boolean> {
|
||||
const buttons = [confirmText == null ? this.i18nService.t("ok") : confirmText];
|
||||
if (cancelText != null) {
|
||||
buttons.push(cancelText);
|
||||
}
|
||||
|
||||
getDeviceString(): string {
|
||||
const device = DeviceType[this.getDevice()].toLowerCase();
|
||||
return device.replace('desktop', '');
|
||||
const result = await ipcRenderer.invoke("showMessageBox", {
|
||||
type: type,
|
||||
title: title,
|
||||
message: title,
|
||||
detail: text,
|
||||
buttons: buttons,
|
||||
cancelId: buttons.length === 2 ? 1 : null,
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
|
||||
return Promise.resolve(result.response === 0);
|
||||
}
|
||||
|
||||
isDev(): boolean {
|
||||
return isDev();
|
||||
}
|
||||
|
||||
isSelfHost(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string, options?: any): void {
|
||||
const type = options ? options.type : null;
|
||||
const clearing = options ? !!options.clearing : false;
|
||||
const clearMs: number = options && options.clearMs ? options.clearMs : null;
|
||||
clipboard.writeText(text, type);
|
||||
if (!clearing) {
|
||||
this.messagingService.send("copiedToClipboard", {
|
||||
clipboardValue: text,
|
||||
clearMs: clearMs,
|
||||
type: type,
|
||||
clearing: clearing,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isFirefox(): boolean {
|
||||
return false;
|
||||
readFromClipboard(options?: any): Promise<string> {
|
||||
const type = options ? options.type : null;
|
||||
return Promise.resolve(clipboard.readText(type));
|
||||
}
|
||||
|
||||
async supportsBiometric(): Promise<boolean> {
|
||||
return await this.stateService.getEnableBiometric();
|
||||
}
|
||||
|
||||
authenticateBiometric(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const val = ipcRenderer.sendSync("biometric", {
|
||||
action: "authenticate",
|
||||
});
|
||||
resolve(val);
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultSystemTheme() {
|
||||
return ipcRenderer.invoke("systemTheme");
|
||||
}
|
||||
|
||||
onDefaultSystemThemeChange(callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown) {
|
||||
ipcRenderer.on("systemThemeUpdated", (event, theme: ThemeType.Light | ThemeType.Dark) =>
|
||||
callback(theme)
|
||||
);
|
||||
}
|
||||
|
||||
async getEffectiveTheme() {
|
||||
const theme = await this.stateService.getTheme();
|
||||
if (theme == null || theme === ThemeType.System) {
|
||||
return this.getDefaultSystemTheme();
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
isChrome(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isEdge(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isOpera(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isVivaldi(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSafari(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isIE(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMacAppStore(): boolean {
|
||||
return isMacAppStore();
|
||||
}
|
||||
|
||||
isViewOpen(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
launchUri(uri: string, options?: any): void {
|
||||
shell.openExternal(uri);
|
||||
}
|
||||
|
||||
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
|
||||
const blob = new Blob([blobData], blobOptions);
|
||||
const a = win.document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = fileName;
|
||||
win.document.body.appendChild(a);
|
||||
a.click();
|
||||
win.document.body.removeChild(a);
|
||||
}
|
||||
|
||||
getApplicationVersion(): Promise<string> {
|
||||
return ipcRenderer.invoke('appVersion');
|
||||
}
|
||||
|
||||
// Temporarily restricted to only Windows until https://github.com/electron/electron/pull/28349
|
||||
// has been merged and an updated electron build is available.
|
||||
supportsWebAuthn(win: Window): boolean {
|
||||
return process.platform === 'win32';
|
||||
}
|
||||
|
||||
supportsDuo(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[],
|
||||
options?: any): void {
|
||||
this.messagingService.send('showToast', {
|
||||
text: text,
|
||||
title: title,
|
||||
type: type,
|
||||
options: options,
|
||||
});
|
||||
}
|
||||
|
||||
async showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string):
|
||||
Promise<boolean> {
|
||||
const buttons = [confirmText == null ? this.i18nService.t('ok') : confirmText];
|
||||
if (cancelText != null) {
|
||||
buttons.push(cancelText);
|
||||
}
|
||||
|
||||
const result = await ipcRenderer.invoke('showMessageBox', {
|
||||
type: type,
|
||||
title: title,
|
||||
message: title,
|
||||
detail: text,
|
||||
buttons: buttons,
|
||||
cancelId: buttons.length === 2 ? 1 : null,
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
|
||||
return Promise.resolve(result.response === 0);
|
||||
}
|
||||
|
||||
isDev(): boolean {
|
||||
return isDev();
|
||||
}
|
||||
|
||||
isSelfHost(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string, options?: any): void {
|
||||
const type = options ? options.type : null;
|
||||
const clearing = options ? !!options.clearing : false;
|
||||
const clearMs: number = options && options.clearMs ? options.clearMs : null;
|
||||
clipboard.writeText(text, type);
|
||||
if (!clearing) {
|
||||
this.messagingService.send('copiedToClipboard', {
|
||||
clipboardValue: text,
|
||||
clearMs: clearMs,
|
||||
type: type,
|
||||
clearing: clearing,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
readFromClipboard(options?: any): Promise<string> {
|
||||
const type = options ? options.type : null;
|
||||
return Promise.resolve(clipboard.readText(type));
|
||||
}
|
||||
|
||||
async supportsBiometric(): Promise<boolean> {
|
||||
return await this.stateService.getEnableBiometric();
|
||||
}
|
||||
|
||||
authenticateBiometric(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const val = ipcRenderer.sendSync('biometric', {
|
||||
action: 'authenticate',
|
||||
});
|
||||
resolve(val);
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultSystemTheme() {
|
||||
return ipcRenderer.invoke('systemTheme');
|
||||
}
|
||||
|
||||
onDefaultSystemThemeChange(callback: ((theme: ThemeType.Light | ThemeType.Dark) => unknown)) {
|
||||
ipcRenderer.on('systemThemeUpdated', (event, theme: ThemeType.Light | ThemeType.Dark) => callback(theme));
|
||||
}
|
||||
|
||||
async getEffectiveTheme() {
|
||||
const theme = await this.stateService.getTheme();
|
||||
if (theme == null || theme === ThemeType.System) {
|
||||
return this.getDefaultSystemTheme();
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
supportsSecureStorage(): boolean {
|
||||
return true;
|
||||
}
|
||||
supportsSecureStorage(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service';
|
||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
||||
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
|
||||
export class ElectronRendererMessagingService implements MessagingService {
|
||||
constructor(private broadcasterService: BroadcasterService) {
|
||||
ipcRenderer.on('messagingService', async (event: any, message: any) => {
|
||||
if (message.command) {
|
||||
this.sendMessage(message.command, message, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
constructor(private broadcasterService: BroadcasterService) {
|
||||
ipcRenderer.on("messagingService", async (event: any, message: any) => {
|
||||
if (message.command) {
|
||||
this.sendMessage(message.command, message, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
this.sendMessage(subscriber, arg, true);
|
||||
}
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
this.sendMessage(subscriber, arg, true);
|
||||
}
|
||||
|
||||
private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
this.broadcasterService.send(message);
|
||||
if (toMain) {
|
||||
ipcRenderer.send('messagingService', message);
|
||||
}
|
||||
private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
this.broadcasterService.send(message);
|
||||
if (toMain) {
|
||||
ipcRenderer.send("messagingService", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||
import { StorageService } from "jslib-common/abstractions/storage.service";
|
||||
|
||||
import { StorageOptions } from 'jslib-common/models/domain/storageOptions';
|
||||
import { StorageOptions } from "jslib-common/models/domain/storageOptions";
|
||||
|
||||
export class ElectronRendererSecureStorageService implements StorageService {
|
||||
async get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||
const val = ipcRenderer.sendSync('keytar', {
|
||||
action: 'getPassword',
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? '',
|
||||
});
|
||||
return Promise.resolve(val != null ? JSON.parse(val) as T : null);
|
||||
}
|
||||
async get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||
const val = ipcRenderer.sendSync("keytar", {
|
||||
action: "getPassword",
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? "",
|
||||
});
|
||||
return Promise.resolve(val != null ? (JSON.parse(val) as T) : null);
|
||||
}
|
||||
|
||||
async has(key: string, options?: StorageOptions): Promise<boolean> {
|
||||
const val = ipcRenderer.sendSync('keytar', {
|
||||
action: 'hasPassword',
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? '',
|
||||
});
|
||||
return Promise.resolve(!!val);
|
||||
}
|
||||
async has(key: string, options?: StorageOptions): Promise<boolean> {
|
||||
const val = ipcRenderer.sendSync("keytar", {
|
||||
action: "hasPassword",
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? "",
|
||||
});
|
||||
return Promise.resolve(!!val);
|
||||
}
|
||||
|
||||
async save(key: string, obj: any, options?: StorageOptions): Promise<any> {
|
||||
ipcRenderer.sendSync('keytar', {
|
||||
action: 'setPassword',
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? '',
|
||||
value: JSON.stringify(obj),
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
async save(key: string, obj: any, options?: StorageOptions): Promise<any> {
|
||||
ipcRenderer.sendSync("keytar", {
|
||||
action: "setPassword",
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? "",
|
||||
value: JSON.stringify(obj),
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async remove(key: string, options?: StorageOptions): Promise<any> {
|
||||
ipcRenderer.sendSync('keytar', {
|
||||
action: 'deletePassword',
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? '',
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
async remove(key: string, options?: StorageOptions): Promise<any> {
|
||||
ipcRenderer.sendSync("keytar", {
|
||||
action: "deletePassword",
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? "",
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||
import { StorageService } from "jslib-common/abstractions/storage.service";
|
||||
|
||||
export class ElectronRendererStorageService implements StorageService {
|
||||
get<T>(key: string): Promise<T> {
|
||||
return ipcRenderer.invoke('storageService', {
|
||||
action: 'get',
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
get<T>(key: string): Promise<T> {
|
||||
return ipcRenderer.invoke("storageService", {
|
||||
action: "get",
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
|
||||
has(key: string): Promise<boolean> {
|
||||
return ipcRenderer.invoke('storageService', {
|
||||
action: 'has',
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
has(key: string): Promise<boolean> {
|
||||
return ipcRenderer.invoke("storageService", {
|
||||
action: "has",
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
return ipcRenderer.invoke('storageService', {
|
||||
action: 'save',
|
||||
key: key,
|
||||
obj: obj,
|
||||
});
|
||||
}
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
return ipcRenderer.invoke("storageService", {
|
||||
action: "save",
|
||||
key: key,
|
||||
obj: obj,
|
||||
});
|
||||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
return ipcRenderer.invoke('storageService', {
|
||||
action: 'remove',
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
remove(key: string): Promise<any> {
|
||||
return ipcRenderer.invoke("storageService", {
|
||||
action: "remove",
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import { ipcMain, ipcRenderer } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import { ipcMain, ipcRenderer } from "electron";
|
||||
import * as fs from "fs";
|
||||
|
||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||
import { StorageService } from "jslib-common/abstractions/storage.service";
|
||||
|
||||
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
|
||||
import { NodeUtils } from "jslib-common/misc/nodeUtils";
|
||||
|
||||
// tslint:disable-next-line
|
||||
const Store = require('electron-store');
|
||||
const Store = require("electron-store");
|
||||
|
||||
export class ElectronStorageService implements StorageService {
|
||||
private store: any;
|
||||
private store: any;
|
||||
|
||||
constructor(dir: string, defaults = {}) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
NodeUtils.mkdirpSync(dir, '700');
|
||||
}
|
||||
const storeConfig: any = {
|
||||
defaults: defaults,
|
||||
name: 'data',
|
||||
};
|
||||
this.store = new Store(storeConfig);
|
||||
|
||||
ipcMain.handle('storageService', (event, options) => {
|
||||
switch (options.action) {
|
||||
case 'get':
|
||||
return this.get(options.key);
|
||||
case 'has':
|
||||
return this.has(options.key);
|
||||
case 'save':
|
||||
return this.save(options.key, options.obj);
|
||||
case 'remove':
|
||||
return this.remove(options.key);
|
||||
}
|
||||
});
|
||||
constructor(dir: string, defaults = {}) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
NodeUtils.mkdirpSync(dir, "700");
|
||||
}
|
||||
const storeConfig: any = {
|
||||
defaults: defaults,
|
||||
name: "data",
|
||||
};
|
||||
this.store = new Store(storeConfig);
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
const val = this.store.get(key) as T;
|
||||
return Promise.resolve(val != null ? val : null);
|
||||
}
|
||||
ipcMain.handle("storageService", (event, options) => {
|
||||
switch (options.action) {
|
||||
case "get":
|
||||
return this.get(options.key);
|
||||
case "has":
|
||||
return this.has(options.key);
|
||||
case "save":
|
||||
return this.save(options.key, options.obj);
|
||||
case "remove":
|
||||
return this.remove(options.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
has(key: string): Promise<boolean> {
|
||||
const val = this.store.get(key);
|
||||
return Promise.resolve(val != null);
|
||||
}
|
||||
get<T>(key: string): Promise<T> {
|
||||
const val = this.store.get(key) as T;
|
||||
return Promise.resolve(val != null ? val : null);
|
||||
}
|
||||
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
if (obj instanceof Set) {
|
||||
obj = Array.from(obj);
|
||||
}
|
||||
this.store.set(key, obj);
|
||||
return Promise.resolve();
|
||||
}
|
||||
has(key: string): Promise<boolean> {
|
||||
const val = this.store.get(key);
|
||||
return Promise.resolve(val != null);
|
||||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
this.store.delete(key);
|
||||
return Promise.resolve();
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
if (obj instanceof Set) {
|
||||
obj = Array.from(obj);
|
||||
}
|
||||
this.store.set(key, obj);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
this.store.delete(key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,185 +1,185 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
nativeImage,
|
||||
Tray,
|
||||
} from 'electron';
|
||||
import * as path from 'path';
|
||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
|
||||
import * as path from "path";
|
||||
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
import { WindowMain } from './window.main';
|
||||
import { WindowMain } from "./window.main";
|
||||
|
||||
export class TrayMain {
|
||||
contextMenu: Menu;
|
||||
contextMenu: Menu;
|
||||
|
||||
private appName: string;
|
||||
private tray: Tray;
|
||||
private icon: string | Electron.NativeImage;
|
||||
private pressedIcon: Electron.NativeImage;
|
||||
private appName: string;
|
||||
private tray: Tray;
|
||||
private icon: string | Electron.NativeImage;
|
||||
private pressedIcon: Electron.NativeImage;
|
||||
|
||||
constructor(private windowMain: WindowMain, private i18nService: I18nService,
|
||||
private stateService: StateService) {
|
||||
if (process.platform === 'win32') {
|
||||
this.icon = path.join(__dirname, '/images/icon.ico');
|
||||
} else if (process.platform === 'darwin') {
|
||||
const nImage = nativeImage.createFromPath(path.join(__dirname, '/images/icon-template.png'));
|
||||
nImage.setTemplateImage(true);
|
||||
this.icon = nImage;
|
||||
this.pressedIcon = nativeImage.createFromPath(path.join(__dirname, '/images/icon-highlight.png'));
|
||||
} else {
|
||||
this.icon = path.join(__dirname, '/images/icon.png');
|
||||
}
|
||||
constructor(
|
||||
private windowMain: WindowMain,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService
|
||||
) {
|
||||
if (process.platform === "win32") {
|
||||
this.icon = path.join(__dirname, "/images/icon.ico");
|
||||
} else if (process.platform === "darwin") {
|
||||
const nImage = nativeImage.createFromPath(path.join(__dirname, "/images/icon-template.png"));
|
||||
nImage.setTemplateImage(true);
|
||||
this.icon = nImage;
|
||||
this.pressedIcon = nativeImage.createFromPath(
|
||||
path.join(__dirname, "/images/icon-highlight.png")
|
||||
);
|
||||
} else {
|
||||
this.icon = path.join(__dirname, "/images/icon.png");
|
||||
}
|
||||
}
|
||||
|
||||
async init(appName: string, additionalMenuItems: MenuItemConstructorOptions[] = null) {
|
||||
this.appName = appName;
|
||||
|
||||
const menuItemOptions: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: this.i18nService.t("showHide"),
|
||||
click: () => this.toggleWindow(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("exit"),
|
||||
click: () => this.closeWindow(),
|
||||
},
|
||||
];
|
||||
|
||||
if (additionalMenuItems != null) {
|
||||
menuItemOptions.splice(1, 0, ...additionalMenuItems);
|
||||
}
|
||||
|
||||
async init(appName: string, additionalMenuItems: MenuItemConstructorOptions[] = null) {
|
||||
this.appName = appName;
|
||||
this.contextMenu = Menu.buildFromTemplate(menuItemOptions);
|
||||
if (await this.stateService.getEnableTray()) {
|
||||
this.showTray();
|
||||
}
|
||||
}
|
||||
|
||||
const menuItemOptions: MenuItemConstructorOptions[] = [{
|
||||
label: this.i18nService.t('showHide'),
|
||||
click: () => this.toggleWindow(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: this.i18nService.t('exit'),
|
||||
click: () => this.closeWindow(),
|
||||
}];
|
||||
setupWindowListeners(win: BrowserWindow) {
|
||||
win.on("minimize", async (e: Event) => {
|
||||
if (await this.stateService.getEnableMinimizeToTray()) {
|
||||
e.preventDefault();
|
||||
this.hideToTray();
|
||||
}
|
||||
});
|
||||
|
||||
if (additionalMenuItems != null) {
|
||||
menuItemOptions.splice(1, 0, ...additionalMenuItems);
|
||||
win.on("close", async (e: Event) => {
|
||||
if (await this.stateService.getEnableCloseToTray()) {
|
||||
if (!this.windowMain.isQuitting) {
|
||||
e.preventDefault();
|
||||
this.hideToTray();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.contextMenu = Menu.buildFromTemplate(menuItemOptions);
|
||||
if (await this.stateService.getEnableTray()) {
|
||||
this.showTray();
|
||||
}
|
||||
win.on("show", async (e: Event) => {
|
||||
const enableTray = await this.stateService.getEnableTray();
|
||||
if (!enableTray) {
|
||||
setTimeout(() => this.removeTray(false), 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeTray(showWindow = true) {
|
||||
// Due to https://github.com/electron/electron/issues/17622
|
||||
// we cannot destroy the tray icon on linux.
|
||||
if (this.tray != null && process.platform !== "linux") {
|
||||
this.tray.destroy();
|
||||
this.tray = null;
|
||||
}
|
||||
|
||||
setupWindowListeners(win: BrowserWindow) {
|
||||
win.on('minimize', async (e: Event) => {
|
||||
if (await this.stateService.getEnableMinimizeToTray()) {
|
||||
e.preventDefault();
|
||||
this.hideToTray();
|
||||
}
|
||||
});
|
||||
|
||||
win.on('close', async (e: Event) => {
|
||||
if (await this.stateService.getEnableCloseToTray()) {
|
||||
if (!this.windowMain.isQuitting) {
|
||||
e.preventDefault();
|
||||
this.hideToTray();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
win.on('show', async (e: Event) => {
|
||||
const enableTray = await this.stateService.getEnableTray();
|
||||
if (!enableTray) {
|
||||
setTimeout(() => this.removeTray(false), 100);
|
||||
}
|
||||
if (showWindow && this.windowMain.win != null && !this.windowMain.win.isVisible()) {
|
||||
this.windowMain.win.show();
|
||||
}
|
||||
}
|
||||
|
||||
async hideToTray() {
|
||||
this.showTray();
|
||||
if (this.windowMain.win != null) {
|
||||
this.windowMain.win.hide();
|
||||
}
|
||||
if (this.isDarwin() && !(await this.stateService.getAlwaysShowDock())) {
|
||||
this.hideDock();
|
||||
}
|
||||
}
|
||||
|
||||
restoreFromTray() {
|
||||
if (this.windowMain.win == null || !this.windowMain.win.isVisible()) {
|
||||
this.toggleWindow();
|
||||
}
|
||||
}
|
||||
|
||||
showTray() {
|
||||
if (this.tray != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tray = new Tray(this.icon);
|
||||
this.tray.setToolTip(this.appName);
|
||||
this.tray.on("click", () => this.toggleWindow());
|
||||
this.tray.on("right-click", () => this.tray.popUpContextMenu(this.contextMenu));
|
||||
|
||||
if (this.pressedIcon != null) {
|
||||
this.tray.setPressedImage(this.pressedIcon);
|
||||
}
|
||||
if (this.contextMenu != null && !this.isDarwin()) {
|
||||
this.tray.setContextMenu(this.contextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
updateContextMenu() {
|
||||
if (this.contextMenu != null && this.isLinux()) {
|
||||
this.tray.setContextMenu(this.contextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
private hideDock() {
|
||||
app.dock.hide();
|
||||
}
|
||||
|
||||
private showDock() {
|
||||
app.dock.show();
|
||||
}
|
||||
|
||||
private isDarwin() {
|
||||
return process.platform === "darwin";
|
||||
}
|
||||
|
||||
private isLinux() {
|
||||
return process.platform === "linux";
|
||||
}
|
||||
|
||||
private async toggleWindow() {
|
||||
if (this.windowMain.win == null) {
|
||||
if (this.isDarwin()) {
|
||||
// On MacOS, closing the window via the red button destroys the BrowserWindow instance.
|
||||
this.windowMain.createWindow().then(() => {
|
||||
this.windowMain.win.show();
|
||||
this.showDock();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
removeTray(showWindow = true) {
|
||||
// Due to https://github.com/electron/electron/issues/17622
|
||||
// we cannot destroy the tray icon on linux.
|
||||
if (this.tray != null && process.platform !== 'linux') {
|
||||
this.tray.destroy();
|
||||
this.tray = null;
|
||||
}
|
||||
|
||||
if (showWindow && this.windowMain.win != null && !this.windowMain.win.isVisible()) {
|
||||
this.windowMain.win.show();
|
||||
}
|
||||
if (this.windowMain.win.isVisible()) {
|
||||
this.windowMain.win.hide();
|
||||
if (this.isDarwin() && !(await this.stateService.getAlwaysShowDock())) {
|
||||
this.hideDock();
|
||||
}
|
||||
} else {
|
||||
this.windowMain.win.show();
|
||||
if (this.isDarwin()) {
|
||||
this.showDock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async hideToTray() {
|
||||
this.showTray();
|
||||
if (this.windowMain.win != null) {
|
||||
this.windowMain.win.hide();
|
||||
}
|
||||
if (this.isDarwin() && !await this.stateService.getAlwaysShowDock()) {
|
||||
this.hideDock();
|
||||
}
|
||||
}
|
||||
|
||||
restoreFromTray() {
|
||||
if (this.windowMain.win == null || !this.windowMain.win.isVisible()) {
|
||||
this.toggleWindow();
|
||||
}
|
||||
}
|
||||
|
||||
showTray() {
|
||||
if (this.tray != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tray = new Tray(this.icon);
|
||||
this.tray.setToolTip(this.appName);
|
||||
this.tray.on('click', () => this.toggleWindow());
|
||||
this.tray.on('right-click', () => this.tray.popUpContextMenu(this.contextMenu));
|
||||
|
||||
if (this.pressedIcon != null) {
|
||||
this.tray.setPressedImage(this.pressedIcon);
|
||||
}
|
||||
if (this.contextMenu != null && !this.isDarwin()) {
|
||||
this.tray.setContextMenu(this.contextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
updateContextMenu() {
|
||||
if (this.contextMenu != null && this.isLinux()) {
|
||||
this.tray.setContextMenu(this.contextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
private hideDock() {
|
||||
app.dock.hide();
|
||||
}
|
||||
|
||||
private showDock() {
|
||||
app.dock.show();
|
||||
}
|
||||
|
||||
private isDarwin() {
|
||||
return process.platform === 'darwin';
|
||||
}
|
||||
|
||||
private isLinux() {
|
||||
return process.platform === 'linux';
|
||||
}
|
||||
|
||||
private async toggleWindow() {
|
||||
if (this.windowMain.win == null) {
|
||||
if (this.isDarwin()) {
|
||||
// On MacOS, closing the window via the red button destroys the BrowserWindow instance.
|
||||
this.windowMain.createWindow().then(() => {
|
||||
this.windowMain.win.show();
|
||||
this.showDock();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.windowMain.win.isVisible()) {
|
||||
this.windowMain.win.hide();
|
||||
if (this.isDarwin() && !await this.stateService.getAlwaysShowDock()) {
|
||||
this.hideDock();
|
||||
}
|
||||
} else {
|
||||
this.windowMain.win.show();
|
||||
if (this.isDarwin()) {
|
||||
this.showDock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private closeWindow() {
|
||||
this.windowMain.isQuitting = true;
|
||||
if (this.windowMain.win != null) {
|
||||
this.windowMain.win.close();
|
||||
}
|
||||
private closeWindow() {
|
||||
this.windowMain.isQuitting = true;
|
||||
if (this.windowMain.win != null) {
|
||||
this.windowMain.win.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +1,154 @@
|
||||
import {
|
||||
dialog,
|
||||
Menu,
|
||||
MenuItem,
|
||||
shell,
|
||||
} from 'electron';
|
||||
import log from 'electron-log';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { dialog, Menu, MenuItem, shell } from "electron";
|
||||
import log from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
import {
|
||||
isAppImage,
|
||||
isDev,
|
||||
isMacAppStore,
|
||||
isWindowsPortable,
|
||||
isWindowsStore,
|
||||
} from './utils';
|
||||
import { isAppImage, isDev, isMacAppStore, isWindowsPortable, isWindowsStore } from "./utils";
|
||||
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { WindowMain } from './window.main';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { WindowMain } from "./window.main";
|
||||
|
||||
const UpdaterCheckInitalDelay = 5 * 1000; // 5 seconds
|
||||
const UpdaterCheckInterval = 12 * 60 * 60 * 1000; // 12 hours
|
||||
|
||||
export class UpdaterMain {
|
||||
private doingUpdateCheck = false;
|
||||
private doingUpdateCheckWithFeedback = false;
|
||||
private canUpdate = false;
|
||||
private doingUpdateCheck = false;
|
||||
private doingUpdateCheckWithFeedback = false;
|
||||
private canUpdate = false;
|
||||
|
||||
constructor(private i18nService: I18nService, private windowMain: WindowMain,
|
||||
private gitHubProject: string, private onCheckingForUpdate: () => void = null,
|
||||
private onReset: () => void = null, private onUpdateDownloaded: () => void = null,
|
||||
private projectName: string) {
|
||||
autoUpdater.logger = log;
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private gitHubProject: string,
|
||||
private onCheckingForUpdate: () => void = null,
|
||||
private onReset: () => void = null,
|
||||
private onUpdateDownloaded: () => void = null,
|
||||
private projectName: string
|
||||
) {
|
||||
autoUpdater.logger = log;
|
||||
|
||||
const linuxCanUpdate = process.platform === 'linux' && isAppImage();
|
||||
const windowsCanUpdate = process.platform === 'win32' && !isWindowsStore() && !isWindowsPortable();
|
||||
const macCanUpdate = process.platform === 'darwin' && !isMacAppStore();
|
||||
this.canUpdate = process.env.ELECTRON_NO_UPDATER !== '1' &&
|
||||
(linuxCanUpdate || windowsCanUpdate || macCanUpdate);
|
||||
}
|
||||
const linuxCanUpdate = process.platform === "linux" && isAppImage();
|
||||
const windowsCanUpdate =
|
||||
process.platform === "win32" && !isWindowsStore() && !isWindowsPortable();
|
||||
const macCanUpdate = process.platform === "darwin" && !isMacAppStore();
|
||||
this.canUpdate =
|
||||
process.env.ELECTRON_NO_UPDATER !== "1" &&
|
||||
(linuxCanUpdate || windowsCanUpdate || macCanUpdate);
|
||||
}
|
||||
|
||||
async init() {
|
||||
global.setTimeout(async () => await this.checkForUpdate(), UpdaterCheckInitalDelay);
|
||||
global.setInterval(async () => await this.checkForUpdate(), UpdaterCheckInterval);
|
||||
async init() {
|
||||
global.setTimeout(async () => await this.checkForUpdate(), UpdaterCheckInitalDelay);
|
||||
global.setInterval(async () => await this.checkForUpdate(), UpdaterCheckInterval);
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
if (this.onCheckingForUpdate != null) {
|
||||
this.onCheckingForUpdate();
|
||||
}
|
||||
this.doingUpdateCheck = true;
|
||||
});
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
if (this.onCheckingForUpdate != null) {
|
||||
this.onCheckingForUpdate();
|
||||
}
|
||||
this.doingUpdateCheck = true;
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', async () => {
|
||||
if (this.doingUpdateCheckWithFeedback) {
|
||||
if (this.windowMain.win == null) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await dialog.showMessageBox(this.windowMain.win, {
|
||||
type: 'info',
|
||||
title: this.i18nService.t(this.projectName) + ' - ' + this.i18nService.t('updateAvailable'),
|
||||
message: this.i18nService.t('updateAvailable'),
|
||||
detail: this.i18nService.t('updateAvailableDesc'),
|
||||
buttons: [this.i18nService.t('yes'), this.i18nService.t('no')],
|
||||
cancelId: 1,
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
autoUpdater.downloadUpdate();
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
if (this.doingUpdateCheckWithFeedback && this.windowMain.win != null) {
|
||||
dialog.showMessageBox(this.windowMain.win, {
|
||||
message: this.i18nService.t('noUpdatesAvailable'),
|
||||
buttons: [this.i18nService.t('ok')],
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.reset();
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', async info => {
|
||||
if (this.onUpdateDownloaded != null) {
|
||||
this.onUpdateDownloaded();
|
||||
}
|
||||
|
||||
if (this.windowMain.win == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await dialog.showMessageBox(this.windowMain.win, {
|
||||
type: 'info',
|
||||
title: this.i18nService.t(this.projectName) + ' - ' + this.i18nService.t('restartToUpdate'),
|
||||
message: this.i18nService.t('restartToUpdate'),
|
||||
detail: this.i18nService.t('restartToUpdateDesc', info.version),
|
||||
buttons: [this.i18nService.t('restart'), this.i18nService.t('later')],
|
||||
cancelId: 1,
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('error', error => {
|
||||
if (this.doingUpdateCheckWithFeedback) {
|
||||
dialog.showErrorBox(this.i18nService.t('updateError'),
|
||||
error == null ? this.i18nService.t('unknown') : (error.stack || error).toString());
|
||||
}
|
||||
|
||||
this.reset();
|
||||
});
|
||||
}
|
||||
|
||||
async checkForUpdate(withFeedback: boolean = false) {
|
||||
if (this.doingUpdateCheck || isDev()) {
|
||||
return;
|
||||
autoUpdater.on("update-available", async () => {
|
||||
if (this.doingUpdateCheckWithFeedback) {
|
||||
if (this.windowMain.win == null) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.canUpdate) {
|
||||
if (withFeedback) {
|
||||
shell.openExternal('https://github.com/bitwarden/' + this.gitHubProject + '/releases');
|
||||
}
|
||||
const result = await dialog.showMessageBox(this.windowMain.win, {
|
||||
type: "info",
|
||||
title:
|
||||
this.i18nService.t(this.projectName) + " - " + this.i18nService.t("updateAvailable"),
|
||||
message: this.i18nService.t("updateAvailable"),
|
||||
detail: this.i18nService.t("updateAvailableDesc"),
|
||||
buttons: [this.i18nService.t("yes"), this.i18nService.t("no")],
|
||||
cancelId: 1,
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
|
||||
return;
|
||||
if (result.response === 0) {
|
||||
autoUpdater.downloadUpdate();
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.doingUpdateCheckWithFeedback = withFeedback;
|
||||
if (withFeedback) {
|
||||
autoUpdater.autoDownload = false;
|
||||
}
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
if (this.doingUpdateCheckWithFeedback && this.windowMain.win != null) {
|
||||
dialog.showMessageBox(this.windowMain.win, {
|
||||
message: this.i18nService.t("noUpdatesAvailable"),
|
||||
buttons: [this.i18nService.t("ok")],
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
}
|
||||
|
||||
await autoUpdater.checkForUpdates();
|
||||
this.reset();
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", async (info) => {
|
||||
if (this.onUpdateDownloaded != null) {
|
||||
this.onUpdateDownloaded();
|
||||
}
|
||||
|
||||
if (this.windowMain.win == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await dialog.showMessageBox(this.windowMain.win, {
|
||||
type: "info",
|
||||
title: this.i18nService.t(this.projectName) + " - " + this.i18nService.t("restartToUpdate"),
|
||||
message: this.i18nService.t("restartToUpdate"),
|
||||
detail: this.i18nService.t("restartToUpdateDesc", info.version),
|
||||
buttons: [this.i18nService.t("restart"), this.i18nService.t("later")],
|
||||
cancelId: 1,
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (error) => {
|
||||
if (this.doingUpdateCheckWithFeedback) {
|
||||
dialog.showErrorBox(
|
||||
this.i18nService.t("updateError"),
|
||||
error == null ? this.i18nService.t("unknown") : (error.stack || error).toString()
|
||||
);
|
||||
}
|
||||
|
||||
this.reset();
|
||||
});
|
||||
}
|
||||
|
||||
async checkForUpdate(withFeedback: boolean = false) {
|
||||
if (this.doingUpdateCheck || isDev()) {
|
||||
return;
|
||||
}
|
||||
|
||||
private reset() {
|
||||
if (this.onReset != null) {
|
||||
this.onReset();
|
||||
}
|
||||
autoUpdater.autoDownload = true;
|
||||
this.doingUpdateCheck = false;
|
||||
if (!this.canUpdate) {
|
||||
if (withFeedback) {
|
||||
shell.openExternal("https://github.com/bitwarden/" + this.gitHubProject + "/releases");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.doingUpdateCheckWithFeedback = withFeedback;
|
||||
if (withFeedback) {
|
||||
autoUpdater.autoDownload = false;
|
||||
}
|
||||
|
||||
await autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
private reset() {
|
||||
if (this.onReset != null) {
|
||||
this.onReset();
|
||||
}
|
||||
autoUpdater.autoDownload = true;
|
||||
this.doingUpdateCheck = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,76 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
export type RendererMenuItem = {label?: string, type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'), click?: () => any};
|
||||
export type RendererMenuItem = {
|
||||
label?: string;
|
||||
type?: "normal" | "separator" | "submenu" | "checkbox" | "radio";
|
||||
click?: () => any;
|
||||
};
|
||||
|
||||
export function invokeMenu(menu: RendererMenuItem[]) {
|
||||
const menuWithoutClick = menu.map(m => {
|
||||
return { label: m.label, type: m.type };
|
||||
});
|
||||
ipcRenderer.invoke('openContextMenu', { menu: menuWithoutClick }).then((i: number) => {
|
||||
if (i !== -1) {
|
||||
menu[i].click();
|
||||
}
|
||||
});
|
||||
const menuWithoutClick = menu.map((m) => {
|
||||
return { label: m.label, type: m.type };
|
||||
});
|
||||
ipcRenderer.invoke("openContextMenu", { menu: menuWithoutClick }).then((i: number) => {
|
||||
if (i !== -1) {
|
||||
menu[i].click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function isDev() {
|
||||
// ref: https://github.com/sindresorhus/electron-is-dev
|
||||
if ('ELECTRON_IS_DEV' in process.env) {
|
||||
return parseInt(process.env.ELECTRON_IS_DEV, 10) === 1;
|
||||
}
|
||||
return (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath));
|
||||
// ref: https://github.com/sindresorhus/electron-is-dev
|
||||
if ("ELECTRON_IS_DEV" in process.env) {
|
||||
return parseInt(process.env.ELECTRON_IS_DEV, 10) === 1;
|
||||
}
|
||||
return process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath);
|
||||
}
|
||||
|
||||
export function isAppImage() {
|
||||
return process.platform === 'linux' && 'APPIMAGE' in process.env;
|
||||
return process.platform === "linux" && "APPIMAGE" in process.env;
|
||||
}
|
||||
|
||||
export function isMac() {
|
||||
return process.platform === 'darwin';
|
||||
return process.platform === "darwin";
|
||||
}
|
||||
|
||||
export function isMacAppStore() {
|
||||
return isMac() && process.mas && process.mas === true;
|
||||
return isMac() && process.mas && process.mas === true;
|
||||
}
|
||||
|
||||
export function isWindowsStore() {
|
||||
const isWindows = process.platform === 'win32';
|
||||
let windowsStore = process.windowsStore;
|
||||
if (isWindows && !windowsStore &&
|
||||
process.resourcesPath.indexOf('8bitSolutionsLLC.bitwardendesktop_') > -1) {
|
||||
windowsStore = true;
|
||||
}
|
||||
return isWindows && windowsStore === true;
|
||||
const isWindows = process.platform === "win32";
|
||||
let windowsStore = process.windowsStore;
|
||||
if (
|
||||
isWindows &&
|
||||
!windowsStore &&
|
||||
process.resourcesPath.indexOf("8bitSolutionsLLC.bitwardendesktop_") > -1
|
||||
) {
|
||||
windowsStore = true;
|
||||
}
|
||||
return isWindows && windowsStore === true;
|
||||
}
|
||||
|
||||
export function isSnapStore() {
|
||||
return process.platform === 'linux' && process.env.SNAP_USER_DATA != null;
|
||||
return process.platform === "linux" && process.env.SNAP_USER_DATA != null;
|
||||
}
|
||||
|
||||
export function isWindowsPortable() {
|
||||
return process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null;
|
||||
return process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize user agent so external resources used by the app can't built data on our users.
|
||||
*/
|
||||
export function cleanUserAgent(userAgent: string): string {
|
||||
const userAgentItem = (startString: string, endString: string) => {
|
||||
const startIndex = userAgent.indexOf(startString);
|
||||
return userAgent.substring(startIndex, userAgent.indexOf(endString, startIndex) + 1);
|
||||
};
|
||||
const systemInformation = '(Windows NT 10.0; Win64; x64)';
|
||||
const userAgentItem = (startString: string, endString: string) => {
|
||||
const startIndex = userAgent.indexOf(startString);
|
||||
return userAgent.substring(startIndex, userAgent.indexOf(endString, startIndex) + 1);
|
||||
};
|
||||
const systemInformation = "(Windows NT 10.0; Win64; x64)";
|
||||
|
||||
// Set system information, remove bitwarden, and electron information
|
||||
return userAgent.replace(userAgentItem('(', ')'), systemInformation)
|
||||
.replace(userAgentItem('Bitwarden', ' '), '')
|
||||
.replace(userAgentItem('Electron', ' '), '');
|
||||
// Set system information, remove bitwarden, and electron information
|
||||
return userAgent
|
||||
.replace(userAgentItem("(", ")"), systemInformation)
|
||||
.replace(userAgentItem("Bitwarden", " "), "")
|
||||
.replace(userAgentItem("Electron", " "), "");
|
||||
}
|
||||
|
||||
@@ -1,291 +1,296 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
screen,
|
||||
} from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import { app, BrowserWindow, screen } from "electron";
|
||||
import * as path from "path";
|
||||
import * as url from "url";
|
||||
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
import {
|
||||
cleanUserAgent,
|
||||
isDev,
|
||||
isMacAppStore,
|
||||
isSnapStore,
|
||||
} from './utils';
|
||||
import { cleanUserAgent, isDev, isMacAppStore, isSnapStore } from "./utils";
|
||||
|
||||
const mainWindowSizeKey = 'mainWindowSize';
|
||||
const mainWindowSizeKey = "mainWindowSize";
|
||||
const WindowEventHandlingDelay = 100;
|
||||
export class WindowMain {
|
||||
win: BrowserWindow;
|
||||
isQuitting: boolean = false;
|
||||
win: BrowserWindow;
|
||||
isQuitting: boolean = false;
|
||||
|
||||
private windowStateChangeTimer: NodeJS.Timer;
|
||||
private windowStates: { [key: string]: any; } = {};
|
||||
private enableAlwaysOnTop: boolean = false;
|
||||
private windowStateChangeTimer: NodeJS.Timer;
|
||||
private windowStates: { [key: string]: any } = {};
|
||||
private enableAlwaysOnTop: boolean = false;
|
||||
|
||||
constructor(private stateService: StateService, private logService: LogService,
|
||||
private hideTitleBar = false, private defaultWidth = 950, private defaultHeight = 600,
|
||||
private argvCallback: (argv: string[]) => void = null,
|
||||
private createWindowCallback: (win: BrowserWindow) => void) { }
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private logService: LogService,
|
||||
private hideTitleBar = false,
|
||||
private defaultWidth = 950,
|
||||
private defaultHeight = 600,
|
||||
private argvCallback: (argv: string[]) => void = null,
|
||||
private createWindowCallback: (win: BrowserWindow) => void
|
||||
) {}
|
||||
|
||||
init(): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
if (!isMacAppStore() && !isSnapStore()) {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
return;
|
||||
} else {
|
||||
app.on('second-instance', (event, argv, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (this.win != null) {
|
||||
if (this.win.isMinimized() || !this.win.isVisible()) {
|
||||
this.win.show();
|
||||
}
|
||||
this.win.focus();
|
||||
}
|
||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||
if (this.argvCallback != null) {
|
||||
this.argvCallback(argv);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This method will be called when Electron is shutting
|
||||
// down the application.
|
||||
app.on('before-quit', () => {
|
||||
this.isQuitting = true;
|
||||
});
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', async () => {
|
||||
await this.createWindow();
|
||||
resolve();
|
||||
if (this.argvCallback != null) {
|
||||
this.argvCallback(process.argv);
|
||||
}
|
||||
});
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// On OS X 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.isQuitting || isMacAppStore()) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', async () => {
|
||||
// On OS X 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) {
|
||||
await this.createWindow();
|
||||
} else {
|
||||
// Show the window when clicking on Dock icon
|
||||
this.win.show();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
// Catch Error
|
||||
// throw e;
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createWindow(): Promise<void> {
|
||||
this.windowStates[mainWindowSizeKey] = await this.getWindowState(mainWindowSizeKey, this.defaultWidth,
|
||||
this.defaultHeight);
|
||||
this.enableAlwaysOnTop = await this.stateService.getEnableAlwaysOnTop();
|
||||
|
||||
// Create the browser window.
|
||||
this.win = new BrowserWindow({
|
||||
width: this.windowStates[mainWindowSizeKey].width,
|
||||
height: this.windowStates[mainWindowSizeKey].height,
|
||||
minWidth: 680,
|
||||
minHeight: 500,
|
||||
x: this.windowStates[mainWindowSizeKey].x,
|
||||
y: this.windowStates[mainWindowSizeKey].y,
|
||||
title: app.name,
|
||||
icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined,
|
||||
titleBarStyle: this.hideTitleBar && process.platform === 'darwin' ? 'hiddenInset' : undefined,
|
||||
show: false,
|
||||
backgroundColor: '#fff',
|
||||
alwaysOnTop: this.enableAlwaysOnTop,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
backgroundThrottling: false,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.windowStates[mainWindowSizeKey].isMaximized) {
|
||||
this.win.maximize();
|
||||
}
|
||||
|
||||
// Show it later since it might need to be maximized.
|
||||
this.win.show();
|
||||
|
||||
// and load the index.html of the app.
|
||||
this.win.loadURL(url.format(
|
||||
{
|
||||
protocol: 'file:',
|
||||
pathname: path.join(__dirname, '/index.html'),
|
||||
slashes: true,
|
||||
}),
|
||||
{
|
||||
userAgent: cleanUserAgent(this.win.webContents.userAgent),
|
||||
}
|
||||
);
|
||||
|
||||
// Open the DevTools.
|
||||
if (isDev()) {
|
||||
this.win.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// Emitted when the window is closed.
|
||||
this.win.on('closed', async () => {
|
||||
await this.updateWindowState(mainWindowSizeKey, this.win);
|
||||
|
||||
// Dereference the window object, usually you would store window
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
this.win = null;
|
||||
});
|
||||
|
||||
this.win.on('close', async () => {
|
||||
await this.updateWindowState(mainWindowSizeKey, this.win);
|
||||
});
|
||||
|
||||
this.win.on('maximize', async () => {
|
||||
await this.updateWindowState(mainWindowSizeKey, this.win);
|
||||
});
|
||||
|
||||
this.win.on('unmaximize', async () => {
|
||||
await this.updateWindowState(mainWindowSizeKey, this.win);
|
||||
});
|
||||
|
||||
this.win.on('resize', () => {
|
||||
this.windowStateChangeHandler(mainWindowSizeKey, this.win);
|
||||
});
|
||||
|
||||
this.win.on('move', () => {
|
||||
this.windowStateChangeHandler(mainWindowSizeKey, this.win);
|
||||
});
|
||||
this.win.on('focus', () => {
|
||||
this.win.webContents.send('messagingService', {
|
||||
command: 'windowIsFocused',
|
||||
windowIsFocused: true,
|
||||
});
|
||||
});
|
||||
|
||||
if (this.createWindowCallback) {
|
||||
this.createWindowCallback(this.win);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleAlwaysOnTop() {
|
||||
this.enableAlwaysOnTop = !this.win.isAlwaysOnTop();
|
||||
this.win.setAlwaysOnTop(this.enableAlwaysOnTop);
|
||||
await this.stateService.setEnableAlwaysOnTop(this.enableAlwaysOnTop);
|
||||
}
|
||||
|
||||
private windowStateChangeHandler(configKey: string, win: BrowserWindow) {
|
||||
global.clearTimeout(this.windowStateChangeTimer);
|
||||
this.windowStateChangeTimer = global.setTimeout(async () => {
|
||||
await this.updateWindowState(configKey, win);
|
||||
}, WindowEventHandlingDelay);
|
||||
}
|
||||
|
||||
private async updateWindowState(configKey: string, win: BrowserWindow) {
|
||||
if (win == null) {
|
||||
init(): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
if (!isMacAppStore() && !isSnapStore()) {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const bounds = win.getBounds();
|
||||
|
||||
if (this.windowStates[configKey] == null) {
|
||||
this.windowStates[configKey] = (await this.stateService.getWindow()).get(configKey);
|
||||
if (this.windowStates[configKey] == null) {
|
||||
this.windowStates[configKey] = {};
|
||||
} else {
|
||||
app.on("second-instance", (event, argv, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (this.win != null) {
|
||||
if (this.win.isMinimized() || !this.win.isVisible()) {
|
||||
this.win.show();
|
||||
}
|
||||
}
|
||||
|
||||
this.windowStates[configKey].isMaximized = win.isMaximized();
|
||||
this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds;
|
||||
|
||||
if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) {
|
||||
this.windowStates[configKey].x = bounds.x;
|
||||
this.windowStates[configKey].y = bounds.y;
|
||||
this.windowStates[configKey].width = bounds.width;
|
||||
this.windowStates[configKey].height = bounds.height;
|
||||
}
|
||||
|
||||
const cachedWindow = await this.stateService.getWindow() ?? new Map<string, any>();
|
||||
cachedWindow.set(configKey, this.windowStates[configKey]);
|
||||
await this.stateService.setWindow(cachedWindow);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getWindowState(configKey: string, defaultWidth: number, defaultHeight: number) {
|
||||
const windowState = await this.stateService.getWindow() ?? new Map<string, any>();
|
||||
let state = windowState.has(configKey) ?
|
||||
windowState.get(configKey) :
|
||||
null;
|
||||
|
||||
const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized);
|
||||
let displayBounds: Electron.Rectangle = null;
|
||||
if (!isValid) {
|
||||
state = {
|
||||
width: defaultWidth,
|
||||
height: defaultHeight,
|
||||
};
|
||||
|
||||
displayBounds = screen.getPrimaryDisplay().bounds;
|
||||
} else if (this.stateHasBounds(state) && state.displayBounds) {
|
||||
// Check if the display where the window was last open is still available
|
||||
displayBounds = screen.getDisplayMatching(state.displayBounds).bounds;
|
||||
|
||||
if (displayBounds.width !== state.displayBounds.width ||
|
||||
displayBounds.height !== state.displayBounds.height ||
|
||||
displayBounds.x !== state.displayBounds.x ||
|
||||
displayBounds.y !== state.displayBounds.y) {
|
||||
state.x = undefined;
|
||||
state.y = undefined;
|
||||
displayBounds = screen.getPrimaryDisplay().bounds;
|
||||
}
|
||||
this.win.focus();
|
||||
}
|
||||
if (process.platform === "win32" || process.platform === "linux") {
|
||||
if (this.argvCallback != null) {
|
||||
this.argvCallback(argv);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (displayBounds != null) {
|
||||
if (state.width > displayBounds.width && state.height > displayBounds.height) {
|
||||
state.isMaximized = true;
|
||||
}
|
||||
// This method will be called when Electron is shutting
|
||||
// down the application.
|
||||
app.on("before-quit", () => {
|
||||
this.isQuitting = true;
|
||||
});
|
||||
|
||||
if (state.width > displayBounds.width) {
|
||||
state.width = displayBounds.width - 10;
|
||||
}
|
||||
if (state.height > displayBounds.height) {
|
||||
state.height = displayBounds.height - 10;
|
||||
}
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
await this.createWindow();
|
||||
resolve();
|
||||
if (this.argvCallback != null) {
|
||||
this.argvCallback(process.argv);
|
||||
}
|
||||
});
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", () => {
|
||||
// On OS X 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.isQuitting || isMacAppStore()) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", async () => {
|
||||
// On OS X 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) {
|
||||
await this.createWindow();
|
||||
} else {
|
||||
// Show the window when clicking on Dock icon
|
||||
this.win.show();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Catch Error
|
||||
// throw e;
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createWindow(): Promise<void> {
|
||||
this.windowStates[mainWindowSizeKey] = await this.getWindowState(
|
||||
mainWindowSizeKey,
|
||||
this.defaultWidth,
|
||||
this.defaultHeight
|
||||
);
|
||||
this.enableAlwaysOnTop = await this.stateService.getEnableAlwaysOnTop();
|
||||
|
||||
// Create the browser window.
|
||||
this.win = new BrowserWindow({
|
||||
width: this.windowStates[mainWindowSizeKey].width,
|
||||
height: this.windowStates[mainWindowSizeKey].height,
|
||||
minWidth: 680,
|
||||
minHeight: 500,
|
||||
x: this.windowStates[mainWindowSizeKey].x,
|
||||
y: this.windowStates[mainWindowSizeKey].y,
|
||||
title: app.name,
|
||||
icon: process.platform === "linux" ? path.join(__dirname, "/images/icon.png") : undefined,
|
||||
titleBarStyle: this.hideTitleBar && process.platform === "darwin" ? "hiddenInset" : undefined,
|
||||
show: false,
|
||||
backgroundColor: "#fff",
|
||||
alwaysOnTop: this.enableAlwaysOnTop,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
backgroundThrottling: false,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.windowStates[mainWindowSizeKey].isMaximized) {
|
||||
this.win.maximize();
|
||||
}
|
||||
|
||||
// Show it later since it might need to be maximized.
|
||||
this.win.show();
|
||||
|
||||
// and load the index.html of the app.
|
||||
this.win.loadURL(
|
||||
url.format({
|
||||
protocol: "file:",
|
||||
pathname: path.join(__dirname, "/index.html"),
|
||||
slashes: true,
|
||||
}),
|
||||
{
|
||||
userAgent: cleanUserAgent(this.win.webContents.userAgent),
|
||||
}
|
||||
);
|
||||
|
||||
// Open the DevTools.
|
||||
if (isDev()) {
|
||||
this.win.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// Emitted when the window is closed.
|
||||
this.win.on("closed", async () => {
|
||||
await this.updateWindowState(mainWindowSizeKey, this.win);
|
||||
|
||||
// Dereference the window object, usually you would store window
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
this.win = null;
|
||||
});
|
||||
|
||||
this.win.on("close", async () => {
|
||||
await this.updateWindowState(mainWindowSizeKey, this.win);
|
||||
});
|
||||
|
||||
this.win.on("maximize", async () => {
|
||||
await this.updateWindowState(mainWindowSizeKey, this.win);
|
||||
});
|
||||
|
||||
this.win.on("unmaximize", async () => {
|
||||
await this.updateWindowState(mainWindowSizeKey, this.win);
|
||||
});
|
||||
|
||||
this.win.on("resize", () => {
|
||||
this.windowStateChangeHandler(mainWindowSizeKey, this.win);
|
||||
});
|
||||
|
||||
this.win.on("move", () => {
|
||||
this.windowStateChangeHandler(mainWindowSizeKey, this.win);
|
||||
});
|
||||
this.win.on("focus", () => {
|
||||
this.win.webContents.send("messagingService", {
|
||||
command: "windowIsFocused",
|
||||
windowIsFocused: true,
|
||||
});
|
||||
});
|
||||
|
||||
if (this.createWindowCallback) {
|
||||
this.createWindowCallback(this.win);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleAlwaysOnTop() {
|
||||
this.enableAlwaysOnTop = !this.win.isAlwaysOnTop();
|
||||
this.win.setAlwaysOnTop(this.enableAlwaysOnTop);
|
||||
await this.stateService.setEnableAlwaysOnTop(this.enableAlwaysOnTop);
|
||||
}
|
||||
|
||||
private windowStateChangeHandler(configKey: string, win: BrowserWindow) {
|
||||
global.clearTimeout(this.windowStateChangeTimer);
|
||||
this.windowStateChangeTimer = global.setTimeout(async () => {
|
||||
await this.updateWindowState(configKey, win);
|
||||
}, WindowEventHandlingDelay);
|
||||
}
|
||||
|
||||
private async updateWindowState(configKey: string, win: BrowserWindow) {
|
||||
if (win == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const bounds = win.getBounds();
|
||||
|
||||
if (this.windowStates[configKey] == null) {
|
||||
this.windowStates[configKey] = (await this.stateService.getWindow()).get(configKey);
|
||||
if (this.windowStates[configKey] == null) {
|
||||
this.windowStates[configKey] = {};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
this.windowStates[configKey].isMaximized = win.isMaximized();
|
||||
this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds;
|
||||
|
||||
if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) {
|
||||
this.windowStates[configKey].x = bounds.x;
|
||||
this.windowStates[configKey].y = bounds.y;
|
||||
this.windowStates[configKey].width = bounds.width;
|
||||
this.windowStates[configKey].height = bounds.height;
|
||||
}
|
||||
|
||||
const cachedWindow = (await this.stateService.getWindow()) ?? new Map<string, any>();
|
||||
cachedWindow.set(configKey, this.windowStates[configKey]);
|
||||
await this.stateService.setWindow(cachedWindow);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getWindowState(configKey: string, defaultWidth: number, defaultHeight: number) {
|
||||
const windowState = (await this.stateService.getWindow()) ?? new Map<string, any>();
|
||||
let state = windowState.has(configKey) ? windowState.get(configKey) : null;
|
||||
|
||||
const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized);
|
||||
let displayBounds: Electron.Rectangle = null;
|
||||
if (!isValid) {
|
||||
state = {
|
||||
width: defaultWidth,
|
||||
height: defaultHeight,
|
||||
};
|
||||
|
||||
displayBounds = screen.getPrimaryDisplay().bounds;
|
||||
} else if (this.stateHasBounds(state) && state.displayBounds) {
|
||||
// Check if the display where the window was last open is still available
|
||||
displayBounds = screen.getDisplayMatching(state.displayBounds).bounds;
|
||||
|
||||
if (
|
||||
displayBounds.width !== state.displayBounds.width ||
|
||||
displayBounds.height !== state.displayBounds.height ||
|
||||
displayBounds.x !== state.displayBounds.x ||
|
||||
displayBounds.y !== state.displayBounds.y
|
||||
) {
|
||||
state.x = undefined;
|
||||
state.y = undefined;
|
||||
displayBounds = screen.getPrimaryDisplay().bounds;
|
||||
}
|
||||
}
|
||||
|
||||
private stateHasBounds(state: any): boolean {
|
||||
return state != null && Number.isInteger(state.x) && Number.isInteger(state.y) &&
|
||||
Number.isInteger(state.width) && state.width > 0 && Number.isInteger(state.height) && state.height > 0;
|
||||
if (displayBounds != null) {
|
||||
if (state.width > displayBounds.width && state.height > displayBounds.height) {
|
||||
state.isMaximized = true;
|
||||
}
|
||||
|
||||
if (state.width > displayBounds.width) {
|
||||
state.width = displayBounds.width - 10;
|
||||
}
|
||||
if (state.height > displayBounds.height) {
|
||||
state.height = displayBounds.height - 10;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private stateHasBounds(state: any): boolean {
|
||||
return (
|
||||
state != null &&
|
||||
Number.isInteger(state.x) &&
|
||||
Number.isInteger(state.y) &&
|
||||
Number.isInteger(state.width) &&
|
||||
state.width > 0 &&
|
||||
Number.isInteger(state.height) &&
|
||||
state.height > 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,9 @@
|
||||
"declarationDir": "dist/types",
|
||||
"outDir": "dist",
|
||||
"paths": {
|
||||
"jslib-common/*": [
|
||||
"../common/src/*"
|
||||
]
|
||||
"jslib-common/*": ["../common/src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"spec"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
"include": ["src", "spec"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user