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"
|
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
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user