mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Prevent duplicate app windows (#5533)
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
1 parent
5c1f0daf50
commit
6451df1f17
19 files changed
+528
-109
No files matched your search
+3
-12
@@ -9,9 +9,7 @@ import lensProxyPortInjectable from "../../../lens-proxy/lens-proxy-port.injecta
|
||||
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
|
||||
import appNameInjectable from "../../../app-paths/app-name/app-name.injectable";
|
||||
import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable";
|
||||
import { delay } from "../../../../common/utils";
|
||||
import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
|
||||
import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable";
|
||||
import waitUntilBundledExtensionsAreLoadedInjectable from "./wait-until-bundled-extensions-are-loaded.injectable";
|
||||
|
||||
const applicationWindowInjectable = getInjectable({
|
||||
id: "application-window",
|
||||
@@ -21,7 +19,7 @@ const applicationWindowInjectable = getInjectable({
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
const applicationName = di.inject(appNameInjectable);
|
||||
const appEventBus = di.inject(appEventBusInjectable);
|
||||
const ipcMain = di.inject(ipcMainInjectable);
|
||||
const waitUntilBundledExtensionsAreLoaded = di.inject(waitUntilBundledExtensionsAreLoadedInjectable);
|
||||
const lensProxyPort = di.inject(lensProxyPortInjectable);
|
||||
|
||||
return createLensWindow({
|
||||
@@ -45,14 +43,7 @@ const applicationWindowInjectable = getInjectable({
|
||||
onDomReady: () => {
|
||||
appEventBus.emit({ name: "app", action: "dom-ready" });
|
||||
},
|
||||
beforeOpen: async () => {
|
||||
const viewHasLoaded = new Promise<void>((resolve) => {
|
||||
ipcMain.once(bundledExtensionsLoaded, () => resolve());
|
||||
});
|
||||
|
||||
await viewHasLoaded;
|
||||
await delay(50); // wait just a bit longer to let the first round of rendering happen
|
||||
},
|
||||
beforeOpen: waitUntilBundledExtensionsAreLoaded,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
+23
-24
@@ -8,7 +8,7 @@ import applicationWindowStateInjectable from "./application-window-state.injecta
|
||||
import { BrowserWindow } from "electron";
|
||||
import { openBrowser } from "../../../../common/utils";
|
||||
import sendToChannelInElectronBrowserWindowInjectable from "./send-to-channel-in-electron-browser-window.injectable";
|
||||
import type { LensWindow } from "./create-lens-window.injectable";
|
||||
import type { ElectronWindow } from "./create-lens-window.injectable";
|
||||
import type { RequireExactlyOne } from "type-fest";
|
||||
|
||||
export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover";
|
||||
@@ -38,21 +38,16 @@ export interface ElectronWindowConfiguration {
|
||||
onDomReady?: () => void;
|
||||
}
|
||||
|
||||
export type CreateElectronWindow = () => Promise<LensWindow>;
|
||||
export type CreateElectronWindowFor = (config: ElectronWindowConfiguration) => CreateElectronWindow;
|
||||
export type CreateElectronWindow = (config: ElectronWindowConfiguration) => ElectronWindow;
|
||||
|
||||
function isFileSource(src: ContentSource): src is FileSource {
|
||||
return typeof (src as FileSource).file === "string";
|
||||
}
|
||||
const createElectronWindowInjectable = getInjectable({
|
||||
id: "create-electron-window",
|
||||
|
||||
const createElectronWindowFor = getInjectable({
|
||||
id: "create-electron-window-for",
|
||||
|
||||
instantiate: (di): CreateElectronWindowFor => {
|
||||
instantiate: (di): CreateElectronWindow => {
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable);
|
||||
|
||||
return (configuration) => async () => {
|
||||
return (configuration) => {
|
||||
const applicationWindowState = di.inject(
|
||||
applicationWindowStateInjectable,
|
||||
{
|
||||
@@ -172,19 +167,23 @@ const createElectronWindowFor = getInjectable({
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
const contentSource = configuration.getContentSource();
|
||||
|
||||
if (isFileSource(contentSource)) {
|
||||
logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from file: ${contentSource.file}...`);
|
||||
await browserWindow.loadFile(contentSource.file);
|
||||
} else {
|
||||
logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${contentSource.url}...`);
|
||||
await browserWindow.loadURL(contentSource.url);
|
||||
}
|
||||
|
||||
await configuration.beforeOpen?.();
|
||||
|
||||
return {
|
||||
loadFile: async (filePath) => {
|
||||
logger.info(
|
||||
`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from file: ${filePath}...`,
|
||||
);
|
||||
|
||||
await browserWindow.loadFile(filePath);
|
||||
},
|
||||
|
||||
loadUrl: async (url) => {
|
||||
logger.info(
|
||||
`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${url}...`,
|
||||
);
|
||||
|
||||
await browserWindow.loadURL(url);
|
||||
},
|
||||
|
||||
show: () => browserWindow.show(),
|
||||
close: () => browserWindow.close(),
|
||||
send: (args) => sendToChannelInLensWindow(browserWindow, args),
|
||||
@@ -195,4 +194,4 @@ const createElectronWindowFor = getInjectable({
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default createElectronWindowFor;
|
||||
export default createElectronWindowInjectable;
|
||||
+57
-16
@@ -3,14 +3,17 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { SendToViewArgs } from "./lens-window-injection-token";
|
||||
import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window-for.injectable";
|
||||
import createElectronWindowForInjectable from "./create-electron-window-for.injectable";
|
||||
import type { LensWindow, SendToViewArgs } from "./lens-window-injection-token";
|
||||
import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window.injectable";
|
||||
import createElectronWindowForInjectable from "./create-electron-window.injectable";
|
||||
import assert from "assert";
|
||||
|
||||
export interface LensWindow {
|
||||
export interface ElectronWindow {
|
||||
show: () => void;
|
||||
close: () => void;
|
||||
send: (args: SendToViewArgs) => void;
|
||||
loadFile: (filePath: string) => Promise<void>;
|
||||
loadUrl: (url: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface LensWindowConfiguration {
|
||||
@@ -33,31 +36,69 @@ const createLensWindowInjectable = getInjectable({
|
||||
id: "create-lens-window",
|
||||
|
||||
instantiate: (di) => {
|
||||
const createElectronWindowFor = di.inject(createElectronWindowForInjectable);
|
||||
const createElectronWindow = di.inject(createElectronWindowForInjectable);
|
||||
|
||||
return (configuration: LensWindowConfiguration) => {
|
||||
let browserWindow: LensWindow | undefined;
|
||||
return (configuration: LensWindowConfiguration): LensWindow => {
|
||||
let browserWindow: ElectronWindow | undefined;
|
||||
|
||||
const createElectronWindow = createElectronWindowFor({
|
||||
...configuration,
|
||||
onClose: () => browserWindow = undefined,
|
||||
});
|
||||
let windowIsShown = false;
|
||||
let windowIsStarting = false;
|
||||
|
||||
const showWindow = () => {
|
||||
assert(browserWindow);
|
||||
|
||||
browserWindow.show();
|
||||
windowIsShown = true;
|
||||
};
|
||||
|
||||
return {
|
||||
get visible() {
|
||||
return !!browserWindow;
|
||||
id: configuration.id,
|
||||
|
||||
get isVisible() {
|
||||
return windowIsShown;
|
||||
},
|
||||
show: async () => {
|
||||
|
||||
get isStarting() {
|
||||
return windowIsStarting;
|
||||
},
|
||||
|
||||
start: async () => {
|
||||
if (!browserWindow) {
|
||||
browserWindow = await createElectronWindow();
|
||||
windowIsStarting = true;
|
||||
|
||||
browserWindow = createElectronWindow({
|
||||
...configuration,
|
||||
onClose: () => {
|
||||
browserWindow = undefined;
|
||||
windowIsShown = false;
|
||||
},
|
||||
});
|
||||
|
||||
const { file: filePathForContent, url: urlForContent } =
|
||||
configuration.getContentSource();
|
||||
|
||||
if (filePathForContent) {
|
||||
await browserWindow.loadFile(filePathForContent);
|
||||
} else if (urlForContent) {
|
||||
await browserWindow.loadUrl(urlForContent);
|
||||
}
|
||||
|
||||
await configuration.beforeOpen?.();
|
||||
}
|
||||
|
||||
browserWindow.show();
|
||||
showWindow();
|
||||
|
||||
windowIsStarting = false;
|
||||
},
|
||||
|
||||
show: showWindow,
|
||||
|
||||
close: () => {
|
||||
browserWindow?.close();
|
||||
browserWindow = undefined;
|
||||
windowIsShown = false;
|
||||
},
|
||||
|
||||
send: (args: SendToViewArgs) => {
|
||||
if (!browserWindow) {
|
||||
throw new Error(`Tried to send message to window "${configuration.id}" but the window was closed`);
|
||||
|
||||
+5
-2
@@ -12,10 +12,13 @@ export interface SendToViewArgs {
|
||||
}
|
||||
|
||||
export interface LensWindow {
|
||||
show: () => Promise<void>;
|
||||
id: string;
|
||||
start: () => Promise<void>;
|
||||
close: () => void;
|
||||
show: () => void;
|
||||
send: (args: SendToViewArgs) => void;
|
||||
visible: boolean;
|
||||
isVisible: boolean;
|
||||
isStarting: boolean;
|
||||
}
|
||||
|
||||
export const lensWindowInjectionToken = getInjectionToken<LensWindow>({
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
|
||||
import { delay } from "../../../../common/utils";
|
||||
import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable";
|
||||
|
||||
const waitUntilBundledExtensionsAreLoadedInjectable = getInjectable({
|
||||
id: "wait-until-bundled-extensions-are-loaded",
|
||||
|
||||
instantiate: (di) => {
|
||||
const ipcMain = di.inject(ipcMainInjectable);
|
||||
|
||||
return async () => {
|
||||
const viewHasLoaded = new Promise<void>((resolve) => {
|
||||
ipcMain.once(bundledExtensionsLoaded, () => resolve());
|
||||
});
|
||||
|
||||
await viewHasLoaded;
|
||||
await delay(50); // wait just a bit longer to let the first round of rendering happen
|
||||
};
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default waitUntilBundledExtensionsAreLoadedInjectable;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { filter } from "lodash/fp";
|
||||
import { lensWindowInjectionToken } from "./application-window/lens-window-injection-token";
|
||||
|
||||
const getVisibleWindowsInjectable = getInjectable({
|
||||
id: "get-visible-windows",
|
||||
|
||||
instantiate: (di) => {
|
||||
const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken);
|
||||
|
||||
return () =>
|
||||
pipeline(
|
||||
getAllLensWindows(),
|
||||
filter((lensWindow) => !!lensWindow.isVisible),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default getVisibleWindowsInjectable;
|
||||
@@ -5,25 +5,36 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import splashWindowInjectable from "./splash-window/splash-window.injectable";
|
||||
import applicationWindowInjectable from "./application-window/application-window.injectable";
|
||||
import { identity, some } from "lodash/fp";
|
||||
const someIsTruthy = some(identity);
|
||||
|
||||
const showApplicationWindowInjectable = getInjectable({
|
||||
id: "show-application-window",
|
||||
|
||||
instantiate: (di) => {
|
||||
const applicationWindow = di.inject(applicationWindowInjectable);
|
||||
|
||||
const splashWindow = di.inject(
|
||||
splashWindowInjectable,
|
||||
);
|
||||
const splashWindow = di.inject(splashWindowInjectable);
|
||||
|
||||
return async () => {
|
||||
if (applicationWindow.visible) {
|
||||
if (applicationWindow.isStarting) {
|
||||
applicationWindow.show();
|
||||
splashWindow.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await splashWindow.show();
|
||||
const windowIsAlreadyBeingShown = someIsTruthy([
|
||||
applicationWindow.isVisible,
|
||||
splashWindow.isStarting,
|
||||
]);
|
||||
|
||||
await applicationWindow.show();
|
||||
if (windowIsAlreadyBeingShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
await splashWindow.start();
|
||||
|
||||
await applicationWindow.start();
|
||||
|
||||
splashWindow.close();
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user