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:
parent
1a0b5c1d79
commit
aac4eb1a3e
@ -20,23 +20,24 @@ import { tracker } from "../common/tracker";
|
||||
import logger from "./logger"
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
let proxyPort: number;
|
||||
let proxyServer: LensProxy;
|
||||
let windowManager: WindowManager;
|
||||
let clusterManager: ClusterManager;
|
||||
|
||||
app.setName(appName);
|
||||
if (!process.env.CICD) {
|
||||
app.setPath("userData", workingDir);
|
||||
}
|
||||
|
||||
let windowManager: WindowManager;
|
||||
let clusterManager: ClusterManager;
|
||||
let proxyServer: LensProxy;
|
||||
|
||||
mangleProxyEnv()
|
||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await shellSync();
|
||||
app.on("ready", async () => {
|
||||
logger.info(`🚀 Starting Lens from "${workingDir}"`)
|
||||
await shellSync();
|
||||
|
||||
tracker.event("app", "start");
|
||||
const updater = new AppUpdater()
|
||||
@ -44,23 +45,22 @@ async function main() {
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
// find free port
|
||||
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
|
||||
// preload isomorphic stores
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
clusterStore.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
|
||||
clusterManager = new ClusterManager(proxyPort);
|
||||
|
||||
@ -70,18 +70,30 @@ async function main() {
|
||||
} catch (error) {
|
||||
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"}`)
|
||||
app.quit();
|
||||
}
|
||||
|
||||
// create window manager and open app
|
||||
windowManager = new WindowManager(proxyPort);
|
||||
}
|
||||
|
||||
app.on("ready", main);
|
||||
|
||||
app.on("will-quit", async (event) => {
|
||||
event.preventDefault(); // To allow mixpanel sending to be executed
|
||||
if (proxyServer) proxyServer.close()
|
||||
if (clusterManager) clusterManager.stop()
|
||||
app.exit();
|
||||
}
|
||||
|
||||
windowManager = new WindowManager(proxyPort);
|
||||
});
|
||||
|
||||
app.on("activate", (event, hasVisibleWindows) => {
|
||||
logger.info('APP:ACTIVATE', { hasVisibleWindows })
|
||||
if (!hasVisibleWindows) {
|
||||
windowManager.initMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Quit app on Cmd+Q (MacOS)
|
||||
app.on("will-quit", (event) => {
|
||||
logger.info('APP:QUIT');
|
||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for mixpanel, GA, etc.)
|
||||
|
||||
if (userStore.preferences.trayEnabled) {
|
||||
return; // with tray the app remains open
|
||||
} else {
|
||||
windowManager?.destroy();
|
||||
clusterManager?.stop();
|
||||
proxyServer?.close();
|
||||
app.exit(); // force quite
|
||||
}
|
||||
})
|
||||
|
||||
@ -76,7 +76,15 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ 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' },
|
||||
{ role: 'quit' }
|
||||
])
|
||||
]),
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' } // close current window
|
||||
]
|
||||
};
|
||||
mt.push(fileMenu)
|
||||
|
||||
@ -82,14 +82,13 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
label: "About Lens",
|
||||
click() {
|
||||
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
||||
windowManager.bringToTop();
|
||||
showAbout(windowManager.mainView);
|
||||
windowManager.runInContextWindow(showAbout);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Preferences",
|
||||
click() {
|
||||
windowManager.bringToTop();
|
||||
async click() {
|
||||
await windowManager.ensureMainWindow()
|
||||
windowManager.navigate(preferencesURL());
|
||||
},
|
||||
},
|
||||
@ -107,10 +106,10 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
return {
|
||||
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
|
||||
toolTip: clusterId,
|
||||
click() {
|
||||
async click() {
|
||||
workspaceStore.setActive(workspace);
|
||||
clusterStore.setActive(clusterId);
|
||||
windowManager.bringToTop();
|
||||
await windowManager.ensureMainWindow()
|
||||
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
|
||||
}
|
||||
}
|
||||
@ -120,15 +119,16 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
},
|
||||
{
|
||||
label: "Check for updates",
|
||||
async click() {
|
||||
click() {
|
||||
windowManager.runInContextWindow(async window => {
|
||||
const result = await AppUpdater.checkForUpdates();
|
||||
if (!result) {
|
||||
windowManager.bringToTop();
|
||||
dialog.showMessageBoxSync({
|
||||
dialog.showMessageBoxSync(window, {
|
||||
message: "No updates available",
|
||||
type: "info",
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@ -2,28 +2,42 @@ import type { ClusterId } from "../common/cluster-store";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { userStore } from "../common/user-store";
|
||||
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 { initMenu } from "./menu";
|
||||
import { initTray } from "./tray";
|
||||
|
||||
export class WindowManager {
|
||||
public mainView: BrowserWindow;
|
||||
protected mainWindow: BrowserWindow;
|
||||
protected splashWindow: BrowserWindow;
|
||||
protected trayWindow: BrowserWindow;
|
||||
protected windowState: windowStateKeeper.State;
|
||||
protected disposers: Record<string, Function> = {};
|
||||
|
||||
@observable activeClusterId: ClusterId;
|
||||
|
||||
constructor(protected proxyPort: number) {
|
||||
this.bindEvents();
|
||||
this.initMenu();
|
||||
this.initTray();
|
||||
this.initMainWindow();
|
||||
}
|
||||
|
||||
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.mainView = new BrowserWindow({
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x, y, width, height,
|
||||
show: false,
|
||||
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||
@ -36,47 +50,100 @@ export class WindowManager {
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
this.windowState.manage(this.mainView);
|
||||
this.windowState.manage(this.mainWindow);
|
||||
|
||||
// open external links in default browser (target=_blank, window.open)
|
||||
this.mainView.webContents.on("new-window", (event, url) => {
|
||||
this.mainWindow.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();
|
||||
// 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 initMenus() {
|
||||
protected async initMenu() {
|
||||
this.disposers.menuAutoUpdater = initMenu(this);
|
||||
}
|
||||
|
||||
protected async initTray() {
|
||||
this.disposers.trayAutoBind = reaction(() => userStore.preferences.trayEnabled, async isEnabled => {
|
||||
if (isEnabled) {
|
||||
this.ensureTrayWindow();
|
||||
this.disposers.trayAutoUpdater = await initTray(this);
|
||||
} else if (this.disposers.trayAutoUpdater) {
|
||||
this.disposers.trayAutoUpdater();
|
||||
this.disposers.trayAutoUpdater = null;
|
||||
this.trayWindow.destroy();
|
||||
this.trayWindow = null;
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true
|
||||
});
|
||||
}
|
||||
|
||||
bringToTop() {
|
||||
this.mainView.show();
|
||||
protected bindEvents() {
|
||||
// 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[] }) {
|
||||
if (frameId) {
|
||||
this.mainView.webContents.sendToFrame(frameId, channel, ...data);
|
||||
this.mainWindow.webContents.sendToFrame(frameId, channel, ...data);
|
||||
} 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() {
|
||||
if (!this.splashWindow) {
|
||||
this.splashWindow = new BrowserWindow({
|
||||
@ -128,9 +184,10 @@ export class WindowManager {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.windowState.unmanage();
|
||||
this.mainWindow.destroy();
|
||||
this.splashWindow.destroy();
|
||||
this.mainView.destroy();
|
||||
this.mainWindow = null;
|
||||
this.splashWindow = null;
|
||||
Object.entries(this.disposers).forEach(([name, dispose]) => {
|
||||
dispose();
|
||||
delete this.disposers[name]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user