mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add reactive tray icon for when update is available (#5567)
This commit is contained in:
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,
|
||||
},
|
||||
)
|
||||
));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user