Add reactive tray icon for when update is available (#5567)

This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-06-08 08:37:19 -04:00
1 parent db65a89ee0
commit 17c7b6a1bf
32 files changed
+399 -241

No files matched your search

@@ -5,31 +5,37 @@
import { getInjectable } from "@ogre-tools/injectable";
import { Menu, Tray } from "electron";
import packageJsonInjectable from "../../../common/vars/package-json.injectable";
import logger from "../../logger";
import { TRAY_LOG_PREFIX } from "../tray";
import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable";
import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { isEmpty, map, filter } from "lodash/fp";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import trayIconPathInjectable from "../tray-icon-path.injectable";
import trayIconPathsInjectable from "../tray-icon-path.injectable";
import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
import { convertToElectronMenuTemplate } from "../reactive-tray-menu-items/converters";
const TRAY_LOG_PREFIX = "[TRAY]";
export interface ElectronTray {
start(): void;
stop(): void;
setMenuItems(menuItems: TrayMenuItem[]): void;
setIconPath(iconPath: string): void;
}
const electronTrayInjectable = getInjectable({
id: "electron-tray",
instantiate: (di) => {
instantiate: (di): ElectronTray => {
const packageJson = di.inject(packageJsonInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const isWindows = di.inject(isWindowsInjectable);
const logger = di.inject(loggerInjectable);
const trayIconPath = di.inject(trayIconPathInjectable);
const trayIconPaths = di.inject(trayIconPathsInjectable);
let tray: Tray;
return {
start: () => {
tray = new Tray(trayIconPath);
tray = new Tray(trayIconPaths.normal);
tray.setToolTip(packageJson.description);
tray.setIgnoreDoubleClickEvents(true);
@@ -41,21 +47,17 @@ const electronTrayInjectable = getInjectable({
});
}
},
stop: () => {
tray.destroy();
},
setMenuItems: (menuItems) => {
const template = convertToElectronMenuTemplate(menuItems);
const menu = Menu.buildFromTemplate(template);
setMenuItems: (items: TrayMenuItem[]) => {
pipeline(
items,
convertToElectronMenuTemplate,
Menu.buildFromTemplate,
(template) => {
tray.setContextMenu(template);
},
);
tray.setContextMenu(menu);
},
setIconPath: (iconPath) => {
tray.setImage(iconPath);
},
};
},
@@ -64,53 +66,3 @@ const electronTrayInjectable = getInjectable({
});
export default electronTrayInjectable;
const convertToElectronMenuTemplate = (trayMenuItems: TrayMenuItem[]) => {
const _toTrayMenuOptions = (parentId: string | null) =>
pipeline(
trayMenuItems,
filter((item) => item.parentId === parentId),
map(
(trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
if (trayMenuItem.separator) {
return { id: trayMenuItem.id, type: "separator" };
}
const childItems = _toTrayMenuOptions(trayMenuItem.id);
return {
id: trayMenuItem.id,
label: trayMenuItem.label?.get(),
enabled: trayMenuItem.enabled.get(),
toolTip: trayMenuItem.tooltip,
...(isEmpty(childItems)
? {
type: "normal",
submenu: _toTrayMenuOptions(trayMenuItem.id),
click: () => {
try {
trayMenuItem.click?.();
} catch (error) {
logger.error(
`${TRAY_LOG_PREFIX}: clicking item "${trayMenuItem.id} failed."`,
{ error },
);
}
},
}
: {
type: "submenu",
submenu: _toTrayMenuOptions(trayMenuItem.id),
}),
};
},
),
);
return _toTrayMenuOptions(null);
};
@@ -0,0 +1,37 @@
/**
* 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 { reaction } from "mobx";
import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import electronTrayInjectable from "../electron-tray/electron-tray.injectable";
import trayIconPathsInjectable from "../tray-icon-path.injectable";
const reactiveTrayMenuIconInjectable = getInjectable({
id: "reactive-tray-menu-icon",
instantiate: (di) => {
const discoveredUpdateVersion = di.inject(discoveredUpdateVersionInjectable);
const electronTray = di.inject(electronTrayInjectable);
const trayIconPaths = di.inject(trayIconPathsInjectable);
return getStartableStoppable("reactive-tray-menu-icon", () => (
reaction(
() => discoveredUpdateVersion.value.get(),
updateVersion => {
if (updateVersion) {
electronTray.setIconPath(trayIconPaths.updateAvailable);
} else {
electronTray.setIconPath(trayIconPaths.normal);
}
},
{
fireImmediately: true,
},
)
));
},
});
export default reactiveTrayMenuIconInjectable;
@@ -0,0 +1,28 @@
/**
* 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 { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import startTrayInjectable from "../electron-tray/start-tray.injectable";
import reactiveTrayMenuIconInjectable from "./reactive.injectable";
const startReactiveTrayMenuIconInjectable = getInjectable({
id: "start-reactive-tray-menu-icon",
instantiate: (di) => {
const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable);
return {
run: async () => {
await reactiveTrayMenuIcon.start();
},
runAfter: di.inject(startTrayInjectable),
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startReactiveTrayMenuIconInjectable;
@@ -0,0 +1,25 @@
/**
* 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 { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
import reactiveTrayMenuIconInjectable from "./reactive.injectable";
const stopReactiveTrayMenuIconInjectable = getInjectable({
id: "stop-reactive-tray-menu-icon",
instantiate: (di) => {
const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable);
return {
run: async () => {
await reactiveTrayMenuIcon.stop();
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopReactiveTrayMenuIconInjectable;
@@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
export function convertToElectronMenuTemplate(trayMenuItems: TrayMenuItem[]): Electron.MenuItemConstructorOptions[] {
const toTrayMenuOptions = (parentId: string | null) => (
trayMenuItems
.filter((item) => item.parentId === parentId)
.map((trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
if (trayMenuItem.separator) {
return { id: trayMenuItem.id, type: "separator" };
}
const childItems = toTrayMenuOptions(trayMenuItem.id);
return {
id: trayMenuItem.id,
label: trayMenuItem.label?.get(),
enabled: trayMenuItem.enabled.get(),
toolTip: trayMenuItem.tooltip,
...(childItems.length === 0
? {
type: "normal",
submenu: toTrayMenuOptions(trayMenuItem.id),
click: trayMenuItem.click,
}
: {
type: "submenu",
submenu: toTrayMenuOptions(trayMenuItem.id),
}),
};
})
);
return toTrayMenuOptions(null);
}
@@ -4,9 +4,9 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import { autorun } from "mobx";
import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable";
import { reaction } from "mobx";
import electronTrayInjectable from "../electron-tray/electron-tray.injectable";
import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable";
const reactiveTrayMenuItemsInjectable = getInjectable({
id: "reactive-tray-menu-items",
@@ -15,9 +15,15 @@ const reactiveTrayMenuItemsInjectable = getInjectable({
const electronTray = di.inject(electronTrayInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
return getStartableStoppable("reactive-tray-menu-items", () => autorun(() => {
electronTray.setMenuItems(trayMenuItems.get());
}));
return getStartableStoppable("reactive-tray-menu-items", () => (
reaction(
() => trayMenuItems.get(),
electronTray.setMenuItems,
{
fireImmediately: true,
},
)
));
},
});
+18 -7
View File
@@ -6,21 +6,32 @@ import { getInjectable } from "@ogre-tools/injectable";
import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable";
import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import isMacInjectable from "../../common/vars/is-mac.injectable";
const trayIconPathInjectable = getInjectable({
id: "tray-icon-path",
export interface TrayIconPaths {
normal: string;
updateAvailable: string;
}
instantiate: (di) => {
const trayIconPathsInjectable = getInjectable({
id: "tray-icon-paths",
instantiate: (di): TrayIconPaths => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const isDevelopment = di.inject(isDevelopmentInjectable);
return getAbsolutePath(
const isMac = di.inject(isMacInjectable);
const baseIconDirectory = getAbsolutePath(
staticFilesDirectory,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
"trayIconTemplate.png",
);
const fileSuffix = isMac ? "Template.png" : ".png";
return {
normal: getAbsolutePath(baseIconDirectory, `trayIcon${fileSuffix}`),
updateAvailable: getAbsolutePath(baseIconDirectory, `trayIconUpdateAvailable${fileSuffix}`),
};
},
});
export default trayIconPathInjectable;
export default trayIconPathsInjectable;
+1 -2
View File
@@ -12,8 +12,7 @@ const trayItemsInjectable = getInjectable({
instantiate: (di) => {
const extensions = di.inject(mainExtensionsInjectable);
return computed(() =>
extensions.get().flatMap(extension => extension.trayMenus));
return computed(() => extensions.get().flatMap(extension => extension.trayMenus));
},
});
-99
View File
@@ -1,99 +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 { Menu, Tray } from "electron";
import type { IComputedValue } from "mobx";
import { autorun } from "mobx";
import logger from "../logger";
import { isWindows } from "../../common/vars";
import type { Disposer } from "../../common/utils";
import { disposer } from "../../common/utils";
import type { TrayMenuItem } from "./tray-menu-item/tray-menu-item-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { filter, isEmpty, map } from "lodash/fp";
export const TRAY_LOG_PREFIX = "[TRAY]";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray | null = null;
export function initTray(
trayMenuItems: IComputedValue<TrayMenuItem[]>,
showApplicationWindow: () => Promise<void>,
trayIconPath: string,
): Disposer {
tray = new Tray(trayIconPath);
tray.setToolTip(packageInfo.description);
tray.setIgnoreDoubleClickEvents(true);
if (isWindows) {
tray.on("click", () => {
showApplicationWindow()
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
});
}
return disposer(
autorun(() => {
try {
const options = toTrayMenuOptions(trayMenuItems.get());
const menu = Menu.buildFromTemplate(options);
tray?.setContextMenu(menu);
} catch (error) {
logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error });
}
}),
() => {
tray?.destroy();
tray = null;
},
);
}
const toTrayMenuOptions = (trayMenuItems: TrayMenuItem[]) => {
const _toTrayMenuOptions = (parentId: string | null) =>
pipeline(
trayMenuItems,
filter((item) => item.parentId === parentId),
map(
(trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
if (trayMenuItem.separator) {
return { id: trayMenuItem.id, type: "separator" };
}
const childItems = _toTrayMenuOptions(trayMenuItem.id);
return {
id: trayMenuItem.id,
label: trayMenuItem.label?.get(),
enabled: trayMenuItem.enabled.get(),
toolTip: trayMenuItem.tooltip,
...(isEmpty(childItems)
? {
type: "normal",
submenu: _toTrayMenuOptions(trayMenuItem.id),
click: () => {
trayMenuItem.click?.();
},
}
: {
type: "submenu",
submenu: _toTrayMenuOptions(trayMenuItem.id),
}),
};
},
),
);
return _toTrayMenuOptions(null);
};