1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

allow to close main window and re-open from dock or tray icon

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-10-07 21:09:54 +03:00
parent 1a0b5c1d79
commit aac4eb1a3e
4 changed files with 179 additions and 100 deletions

View File

@ -20,23 +20,24 @@ import { tracker } from "../common/tracker";
import logger from "./logger" import logger from "./logger"
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
let proxyServer: LensProxy;
let windowManager: WindowManager;
let clusterManager: ClusterManager;
app.setName(appName); app.setName(appName);
if (!process.env.CICD) { if (!process.env.CICD) {
app.setPath("userData", workingDir); app.setPath("userData", workingDir);
} }
let windowManager: WindowManager;
let clusterManager: ClusterManager;
let proxyServer: LensProxy;
mangleProxyEnv() mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") { if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
} }
async function main() { app.on("ready", async () => {
await shellSync();
logger.info(`🚀 Starting Lens from "${workingDir}"`) logger.info(`🚀 Starting Lens from "${workingDir}"`)
await shellSync();
tracker.event("app", "start"); tracker.event("app", "start");
const updater = new AppUpdater() const updater = new AppUpdater()
@ -44,23 +45,22 @@ async function main() {
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
// find free port // preload isomorphic stores
let proxyPort: number
try {
proxyPort = await getFreePort()
} catch (error) {
logger.error(error)
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
app.quit();
}
// preload configuration from stores
await Promise.all([ await Promise.all([
userStore.load(), userStore.load(),
clusterStore.load(), clusterStore.load(),
workspaceStore.load(), workspaceStore.load(),
]); ]);
// find free port
try {
proxyPort = await getFreePort()
} catch (error) {
logger.error(error)
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
app.exit();
}
// create cluster manager // create cluster manager
clusterManager = new ClusterManager(proxyPort); clusterManager = new ClusterManager(proxyPort);
@ -70,18 +70,30 @@ async function main() {
} catch (error) { } catch (error) {
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`) logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`)
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`) dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`)
app.quit(); app.exit();
} }
// create window manager and open app
windowManager = new WindowManager(proxyPort); windowManager = new WindowManager(proxyPort);
} });
app.on("ready", main); app.on("activate", (event, hasVisibleWindows) => {
logger.info('APP:ACTIVATE', { hasVisibleWindows })
if (!hasVisibleWindows) {
windowManager.initMainWindow();
}
});
app.on("will-quit", async (event) => { // Quit app on Cmd+Q (MacOS)
event.preventDefault(); // To allow mixpanel sending to be executed app.on("will-quit", (event) => {
if (proxyServer) proxyServer.close() logger.info('APP:QUIT');
if (clusterManager) clusterManager.stop() event.preventDefault(); // prevent app's default shutdown (e.g. required for mixpanel, GA, etc.)
app.exit();
if (userStore.preferences.trayEnabled) {
return; // with tray the app remains open
} else {
windowManager?.destroy();
clusterManager?.stop();
proxyServer?.close();
app.exit(); // force quite
}
}) })

View File

@ -76,7 +76,15 @@ export function buildMenu(windowManager: WindowManager) {
{ role: 'hideOthers' }, { role: 'hideOthers' },
{ role: 'unhide' }, { role: 'unhide' },
{ type: 'separator' }, { type: 'separator' },
{ role: 'quit' } {
label: 'Force Quit',
accelerator: 'Cmd+Shift+Q',
click() {
app.exit(0); // force quit since might be blocked within app.on("will-quit")
}
},
{ type: 'separator' },
{ role: 'quit' },
] ]
}; };
@ -118,7 +126,9 @@ export function buildMenu(windowManager: WindowManager) {
}, },
{ type: 'separator' }, { type: 'separator' },
{ role: 'quit' } { role: 'quit' }
]) ]),
{ type: 'separator' },
{ role: 'close' } // close current window
] ]
}; };
mt.push(fileMenu) mt.push(fileMenu)

View File

@ -82,14 +82,13 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
label: "About Lens", label: "About Lens",
click() { click() {
// note: argument[1] (browserWindow) not available when app is not focused / hidden // note: argument[1] (browserWindow) not available when app is not focused / hidden
windowManager.bringToTop(); windowManager.runInContextWindow(showAbout);
showAbout(windowManager.mainView);
}, },
}, },
{ {
label: "Preferences", label: "Preferences",
click() { async click() {
windowManager.bringToTop(); await windowManager.ensureMainWindow()
windowManager.navigate(preferencesURL()); windowManager.navigate(preferencesURL());
}, },
}, },
@ -107,10 +106,10 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
return { return {
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`, label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
toolTip: clusterId, toolTip: clusterId,
click() { async click() {
workspaceStore.setActive(workspace); workspaceStore.setActive(workspace);
clusterStore.setActive(clusterId); clusterStore.setActive(clusterId);
windowManager.bringToTop(); await windowManager.ensureMainWindow()
windowManager.navigate(clusterViewURL({ params: { clusterId } })); windowManager.navigate(clusterViewURL({ params: { clusterId } }));
} }
} }
@ -120,15 +119,16 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
}, },
{ {
label: "Check for updates", label: "Check for updates",
async click() { click() {
const result = await AppUpdater.checkForUpdates(); windowManager.runInContextWindow(async window => {
if (!result) { const result = await AppUpdater.checkForUpdates();
windowManager.bringToTop(); if (!result) {
dialog.showMessageBoxSync({ dialog.showMessageBoxSync(window, {
message: "No updates available", message: "No updates available",
type: "info", type: "info",
}) })
} }
})
}, },
}, },
]); ]);

View File

@ -2,81 +2,148 @@ import type { ClusterId } from "../common/cluster-store";
import { clusterStore } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store";
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
import { observable, reaction } from "mobx"; import { observable, reaction } from "mobx";
import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
import windowStateKeeper from "electron-window-state" import windowStateKeeper from "electron-window-state"
import { initMenu } from "./menu"; import { initMenu } from "./menu";
import { initTray } from "./tray"; import { initTray } from "./tray";
export class WindowManager { export class WindowManager {
public mainView: BrowserWindow; protected mainWindow: BrowserWindow;
protected splashWindow: BrowserWindow; protected splashWindow: BrowserWindow;
protected trayWindow: BrowserWindow;
protected windowState: windowStateKeeper.State; protected windowState: windowStateKeeper.State;
protected disposers: Record<string, Function> = {}; protected disposers: Record<string, Function> = {};
@observable activeClusterId: ClusterId; @observable activeClusterId: ClusterId;
constructor(protected proxyPort: number) { constructor(protected proxyPort: number) {
// Manage main window size and position with state persistence this.bindEvents();
this.windowState = windowStateKeeper({ this.initMenu();
defaultHeight: 900, this.initTray();
defaultWidth: 1440, this.initMainWindow();
});
const { width, height, x, y } = this.windowState;
this.mainView = new BrowserWindow({
x, y, width, height,
show: false,
minWidth: 700, // accommodate 800 x 600 display minimum
minHeight: 500, // accommodate 800 x 600 display minimum
titleBarStyle: "hidden",
backgroundColor: "#1e2124",
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
enableRemoteModule: true,
},
});
this.windowState.manage(this.mainView);
// open external links in default browser (target=_blank, window.open)
this.mainView.webContents.on("new-window", (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
// track visible cluster from ui
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId;
});
// load & show app
this.showMain();
this.initMenus();
} }
protected async initMenus() { get mainUrl() {
return `http://localhost:${this.proxyPort}`
}
async initMainWindow(showSplash = true) {
// Manage main window size and position with state persistence
if (!this.windowState) {
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
}
if (!this.mainWindow) {
const { width, height, x, y } = this.windowState;
this.mainWindow = new BrowserWindow({
x, y, width, height,
show: false,
minWidth: 700, // accommodate 800 x 600 display minimum
minHeight: 500, // accommodate 800 x 600 display minimum
titleBarStyle: "hidden",
backgroundColor: "#1e2124",
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
enableRemoteModule: true,
},
});
this.windowState.manage(this.mainWindow);
// open external links in default browser (target=_blank, window.open)
this.mainWindow.webContents.on("new-window", (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
// clean up
this.mainWindow.on("closed", () => {
this.windowState.unmanage();
this.mainWindow = null;
this.splashWindow = null;
this.trayWindow = null;
})
}
try {
if (showSplash) await this.showSplash();
await this.mainWindow.loadURL(this.mainUrl);
this.mainWindow.show();
this.splashWindow.hide()
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString())
}
}
protected async initMenu() {
this.disposers.menuAutoUpdater = initMenu(this); this.disposers.menuAutoUpdater = initMenu(this);
}
protected async initTray() {
this.disposers.trayAutoBind = reaction(() => userStore.preferences.trayEnabled, async isEnabled => { this.disposers.trayAutoBind = reaction(() => userStore.preferences.trayEnabled, async isEnabled => {
if (isEnabled) { if (isEnabled) {
this.ensureTrayWindow();
this.disposers.trayAutoUpdater = await initTray(this); this.disposers.trayAutoUpdater = await initTray(this);
} else if (this.disposers.trayAutoUpdater) { } else if (this.disposers.trayAutoUpdater) {
this.disposers.trayAutoUpdater(); this.disposers.trayAutoUpdater();
this.disposers.trayAutoUpdater = null; this.trayWindow.destroy();
this.trayWindow = null;
} }
}, { }, {
fireImmediately: true fireImmediately: true
}); });
} }
bringToTop() { protected bindEvents() {
this.mainView.show(); // track visible cluster from ui
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId;
});
}
async ensureMainWindow({ bringToTop = true, showSplash = true } = {}) {
if (!this.mainWindow) {
await this.initMainWindow(showSplash);
}
if (bringToTop) {
this.mainWindow.show();
} else {
this.mainWindow.hide();
}
}
ensureTrayWindow() {
if (!this.trayWindow) {
this.trayWindow = new BrowserWindow({
show: false,
transparent: true,
titleBarStyle: "hidden",
});
}
}
async runInContextWindow(callback: (window: BrowserWindow) => any | Promise<any>) {
const isMainVisible = this.mainWindow?.isVisible(); // is open, but might be not on the top
if (isMainVisible) {
this.mainWindow.show();
await callback(this.mainWindow);
} else {
this.ensureTrayWindow();
if (this.mainWindow) this.mainWindow.hide();
this.trayWindow.show();
await callback(this.trayWindow);
this.trayWindow.hide();
if (this.mainWindow) this.mainWindow.hide();
app.hide();
}
} }
sendToView({ channel, frameId, data = [] }: { channel: string, frameId?: number, data?: any[] }) { sendToView({ channel, frameId, data = [] }: { channel: string, frameId?: number, data?: any[] }) {
if (frameId) { if (frameId) {
this.mainView.webContents.sendToFrame(frameId, channel, ...data); this.mainWindow.webContents.sendToFrame(frameId, channel, ...data);
} else { } else {
this.mainView.webContents.send(channel, ...data); this.mainWindow.webContents.send(channel, ...data);
} }
} }
@ -97,17 +164,6 @@ export class WindowManager {
} }
} }
async showMain() {
try {
await this.showSplash();
await this.mainView.loadURL(`http://localhost:${this.proxyPort}`)
this.mainView.show();
this.splashWindow.close();
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString())
}
}
async showSplash() { async showSplash() {
if (!this.splashWindow) { if (!this.splashWindow) {
this.splashWindow = new BrowserWindow({ this.splashWindow = new BrowserWindow({
@ -128,9 +184,10 @@ export class WindowManager {
} }
destroy() { destroy() {
this.windowState.unmanage(); this.mainWindow.destroy();
this.splashWindow.destroy(); this.splashWindow.destroy();
this.mainView.destroy(); this.mainWindow = null;
this.splashWindow = null;
Object.entries(this.disposers).forEach(([name, dispose]) => { Object.entries(this.disposers).forEach(([name, dispose]) => {
dispose(); dispose();
delete this.disposers[name] delete this.disposers[name]