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

make tray injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-04-20 09:06:35 -04:00
parent ef6a424ec2
commit f255aa62fd
6 changed files with 201 additions and 197 deletions

View File

@ -39,7 +39,7 @@ import { initializeSentryReporting } from "../common/sentry";
import { ensureDir } from "fs-extra";
import { initMenu } from "./menu/menu";
import { kubeApiUpgradeRequest } from "./proxy-functions";
import { initTray } from "./tray/tray";
import initTrayInjectable from "./tray/tray.injectable";
import { ShellSession } from "./shell-session/shell-session";
import { getDi } from "./getDi";
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
@ -53,10 +53,8 @@ import clusterStoreInjectable from "../common/cluster-store/cluster-store.inject
import routerInjectable from "./router/router.injectable";
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
import userStoreInjectable from "../common/user-store/user-store.injectable";
import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
import { broadcastNativeThemeOnUpdate } from "./native-theme";
import windowManagerInjectable from "./window-manager.injectable";
import navigateToPreferencesInjectable from "../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import syncGeneralCatalogEntitiesInjectable from "./catalog-sources/sync-general-catalog-entities.injectable";
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
import applicationMenuItemsInjectable from "./menu/application-menu-items.injectable";
@ -300,12 +298,11 @@ async function main(di: DiContainer) {
const windowManager = di.inject(windowManagerInjectable);
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const cleanupTray = await di.inject(initTrayInjectable);
onQuitCleanup.push(
initMenu(applicationMenuItems),
await initTray(windowManager, trayMenuItems, navigateToPreferences),
cleanupTray,
() => ShellSession.cleanup(),
);

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { type NativeImage, nativeImage, nativeTheme, type Tray } from "electron";
import { JSDOM } from "jsdom";
import sharp from "sharp";
import { getOrInsertWithAsync, base64, type Disposer } from "../../common/utils";
import LogoLens from "../../renderer/components/icon/logo-lens.svg";
export interface CreateTrayIconArgs {
shouldUseDarkColors: boolean;
size: number;
sourceSvg: string;
}
const trayIcons = new Map<boolean, NativeImage>();
async function createTrayIcon({ shouldUseDarkColors, size, sourceSvg }: CreateTrayIconArgs): Promise<NativeImage> {
return getOrInsertWithAsync(trayIcons, shouldUseDarkColors, async () => {
const trayIconColor = shouldUseDarkColors ? "white" : "black"; // Invert to show contrast
const parsedSvg = base64.decode(sourceSvg.split("base64,")[1]);
const svgDom = new JSDOM(`<body>${parsedSvg}</body>`);
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
const iconBuffer = await sharp(Buffer.from(svgRoot.outerHTML))
.resize({ width: size, height: size })
.png()
.toBuffer();
return nativeImage.createFromBuffer(iconBuffer);
});
}
export function createCurrentTrayIcon() {
return createTrayIcon({
shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
size: 16,
sourceSvg: LogoLens,
});
}
export function watchShouldUseDarkColors(tray: Tray): Disposer {
let prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors;
const onUpdated = () => {
if (prevShouldUseDarkColors !== nativeTheme.shouldUseDarkColors) {
prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors;
createCurrentTrayIcon()
.then(img => tray.setImage(img));
}
};
nativeTheme.on("updated", onUpdated);
return () => nativeTheme.off("updated", onUpdated);
}

View File

@ -8,12 +8,10 @@ import mainExtensionsInjectable from "../../extensions/main-extensions.injectabl
const trayItemsInjectable = getInjectable({
id: "tray-items",
instantiate: (di) => {
const extensions = di.inject(mainExtensionsInjectable);
return computed(() =>
extensions.get().flatMap(extension => extension.trayMenus));
return computed(() => extensions.get().flatMap(extension => extension.trayMenus));
},
});

View File

@ -0,0 +1,84 @@
/**
* 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 { computed } from "mobx";
import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import showAboutInjectable from "../menu/show-about.injectable";
import windowManagerInjectable from "../window-manager.injectable";
import trayMenuItemsInjectable from "./tray-menu-items.injectable";
import type { TrayMenuRegistration } from "./tray-menu-registration";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import logger from "../logger";
import { productName } from "../../common/vars";
import { exitApp } from "../exit-app";
import { TRAY_LOG_PREFIX } from "./tray.injectable";
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions {
return {
...trayItem,
submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined,
click: trayItem.click ? () => {
trayItem.click(trayItem);
} : undefined,
};
}
const trayMenuInjectable = getInjectable({
id: "tray-menu",
instantiate: (di) => {
const windowManager = di.inject(windowManagerInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const showAbout = di.inject(showAboutInjectable);
return computed((): Electron.MenuItemConstructorOptions[] => [
{
label: `Open ${productName}`,
click() {
windowManager
.ensureMainWindow()
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
},
},
{
label: "Preferences",
click() {
navigateToPreferences();
},
},
...(
isAutoUpdateEnabled()
? [
{
label: "Check for updates",
click() {
checkForUpdates()
.then(() => windowManager.ensureMainWindow());
},
},
]
: []
),
...trayMenuItems.get().map(getMenuItemConstructorOptions),
{
label: `About ${productName}`,
click() {
windowManager.ensureMainWindow()
.then(showAbout)
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to show Lens About view`, { error }));
},
},
{ type: "separator" } as const,
{
label: "Quit App",
click() {
exitApp();
},
},
]);
},
});
export default trayMenuInjectable;

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import packageInfo from "../../../package.json";
import { Menu, Tray } from "electron";
import { reaction } from "mobx";
import logger from "../logger";
import { isWindows } from "../../common/vars";
import type { Disposer } from "../../common/utils";
import { disposer } from "../../common/utils";
import { getInjectable } from "@ogre-tools/injectable";
import windowManagerInjectable from "../window-manager.injectable";
import trayMenuInjectable from "./tray-menu.injectable";
import { createCurrentTrayIcon, watchShouldUseDarkColors } from "./create-icon";
export const TRAY_LOG_PREFIX = "[TRAY]";
const initTrayInjectable = getInjectable({
id: "init-tray",
instantiate: async (di): Promise<Disposer> => {
const windowManager = di.inject(windowManagerInjectable);
const trayMenu = di.inject(trayMenuInjectable);
const icon = await createCurrentTrayIcon();
const tray = new Tray(icon);
tray.setToolTip(packageInfo.description);
tray.setIgnoreDoubleClickEvents(true);
if (isWindows) {
tray.on("click", () => {
windowManager
.ensureMainWindow()
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
});
}
return disposer(
watchShouldUseDarkColors(tray),
() => tray.destroy(),
reaction(
() => trayMenu.get(),
menuItemOptions => tray.setContextMenu(Menu.buildFromTemplate(menuItemOptions)),
{
fireImmediately: true,
},
),
);
},
});
export default initTrayInjectable;

View File

@ -1,188 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import packageInfo from "../../../package.json";
import type { NativeImage } from "electron";
import { Menu, nativeImage, nativeTheme, Tray } from "electron";
import type { IComputedValue } from "mobx";
import { autorun } from "mobx";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import type { WindowManager } from "../window-manager";
import logger from "../logger";
import { isWindows, productName } from "../../common/vars";
import { exitApp } from "../exit-app";
import type { Disposer } from "../../common/utils";
import { base64, disposer, getOrInsertWithAsync, toJS } from "../../common/utils";
import type { TrayMenuRegistration } from "./tray-menu-registration";
import sharp from "sharp";
import LogoLens from "../../renderer/components/icon/logo-lens.svg";
import { JSDOM } from "jsdom";
import type { ShowAbout } from "../menu/show-about.injectable";
const TRAY_LOG_PREFIX = "[TRAY]";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray;
interface CreateTrayIconArgs {
shouldUseDarkColors: boolean;
size: number;
sourceSvg: string;
}
const trayIcons = new Map<boolean, NativeImage>();
async function createTrayIcon({ shouldUseDarkColors, size, sourceSvg }: CreateTrayIconArgs): Promise<NativeImage> {
return getOrInsertWithAsync(trayIcons, shouldUseDarkColors, async () => {
const trayIconColor = shouldUseDarkColors ? "white" : "black"; // Invert to show contrast
const parsedSvg = base64.decode(sourceSvg.split("base64,")[1]);
const svgDom = new JSDOM(`<body>${parsedSvg}</body>`);
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
const iconBuffer = await sharp(Buffer.from(svgRoot.outerHTML))
.resize({ width: size, height: size })
.png()
.toBuffer();
return nativeImage.createFromBuffer(iconBuffer);
});
}
function createCurrentTrayIcon() {
return createTrayIcon({
shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
size: 16,
sourceSvg: LogoLens,
});
}
function watchShouldUseDarkColors(tray: Tray): Disposer {
let prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors;
const onUpdated = () => {
if (prevShouldUseDarkColors !== nativeTheme.shouldUseDarkColors) {
prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors;
createCurrentTrayIcon()
.then(img => tray.setImage(img));
}
};
nativeTheme.on("updated", onUpdated);
return () => nativeTheme.off("updated", onUpdated);
}
export async function initTray(
windowManager: WindowManager,
trayMenuItems: IComputedValue<TrayMenuRegistration[]>,
navigateToPreferences: () => void,
showAbout: ShowAbout,
): Promise<Disposer> {
const icon = await createCurrentTrayIcon();
const dispose = disposer();
tray = new Tray(icon);
tray.setToolTip(packageInfo.description);
tray.setIgnoreDoubleClickEvents(true);
dispose.push(watchShouldUseDarkColors(tray));
if (isWindows) {
tray.on("click", () => {
windowManager
.ensureMainWindow()
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
});
}
dispose.push(
autorun(() => {
try {
const menu = createTrayMenu(
windowManager,
toJS(trayMenuItems.get()),
navigateToPreferences,
showAbout,
);
tray.setContextMenu(menu);
} catch (error) {
logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error });
}
}),
() => {
tray?.destroy();
tray = null;
},
);
return dispose;
}
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions {
return {
...trayItem,
submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined,
click: trayItem.click ? () => {
trayItem.click(trayItem);
} : undefined,
};
}
function createTrayMenu(
windowManager: WindowManager,
extensionTrayItems: TrayMenuRegistration[],
navigateToPreferences: () => void,
showAbout: ShowAbout,
): Menu {
let template: Electron.MenuItemConstructorOptions[] = [
{
label: `Open ${productName}`,
click() {
windowManager
.ensureMainWindow()
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
},
},
{
label: "Preferences",
click() {
navigateToPreferences();
},
},
];
if (isAutoUpdateEnabled()) {
template.push({
label: "Check for updates",
click() {
checkForUpdates()
.then(() => windowManager.ensureMainWindow());
},
});
}
template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions));
return Menu.buildFromTemplate(template.concat([
{
label: `About ${productName}`,
click() {
windowManager.ensureMainWindow()
.then(showAbout)
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to show Lens About view`, { error }));
},
},
{ type: "separator" },
{
label: "Quit App",
click() {
exitApp();
},
},
]));
}