Add reactive tray icon for when update is available
Signed-off-by: Sebastian Malton <sebastian@malton.name>
1
Makefile
@ -56,6 +56,7 @@ integration: build
|
||||
build: node_modules binaries/client
|
||||
yarn run npm:fix-build-version
|
||||
$(MAKE) build-extensions -B
|
||||
yarn run build:tray-icons
|
||||
yarn run compile
|
||||
ifeq "$(DETECTED_OS)" "Windows"
|
||||
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
||||
|
||||
@ -3,8 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
import { ensureDirSync } from "fs-extra";
|
||||
import { ensureDir, readFile } from "fs-extra";
|
||||
import { JSDOM } from "jsdom";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
@ -12,39 +11,123 @@ import sharp from "sharp";
|
||||
const size = Number(process.env.OUTPUT_SIZE || "16");
|
||||
const outputFolder = process.env.OUTPUT_DIR || "./build/tray";
|
||||
const inputFile = process.env.INPUT_SVG_PATH || "./src/renderer/components/icon/logo-lens.svg";
|
||||
const noticeFile = process.env.NOTICE_SVG_PATH || "./src/renderer/components/icon/notice.svg";
|
||||
|
||||
const svgData = readFileSync(inputFile, { encoding: "utf-8" });
|
||||
const svgDom = new JSDOM(`<body>${svgData}</body>`);
|
||||
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
||||
async function ensureOutputFoler() {
|
||||
await ensureDir(outputFolder);
|
||||
}
|
||||
|
||||
svgRoot.innerHTML += `<style>* {fill: white !important;}</style>`;
|
||||
const lightTemplate = svgRoot.outerHTML;
|
||||
function getSvgStyling(colouring: "dark" | "light"): string {
|
||||
return `
|
||||
<style>
|
||||
ellipse {
|
||||
stroke: ${colouring === "dark" ? "white" : "black"} !important;
|
||||
fill: ${colouring === "dark" ? "black" : "white"} !important;
|
||||
}
|
||||
path, rect {
|
||||
fill: ${colouring === "dark" ? "white" : "black"} !important;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
svgRoot.innerHTML += `<style>* {fill: black !important;}</style>`;
|
||||
async function getBaseIconTemplates() {
|
||||
const svgData = await readFile(inputFile, { encoding: "utf-8" });
|
||||
|
||||
const darkTemplate = svgRoot.outerHTML;
|
||||
const darkDom = new JSDOM(`<body>${svgData}</body>`);
|
||||
const darkRoot = darkDom.window.document.body.getElementsByTagName("svg")[0];
|
||||
|
||||
console.log("Generating tray icon pngs");
|
||||
darkRoot.innerHTML += getSvgStyling("dark");
|
||||
|
||||
ensureDirSync(outputFolder);
|
||||
const lightDom = new JSDOM(`<body>${svgData}</body>`);
|
||||
const lightRoot = lightDom.window.document.body.getElementsByTagName("svg")[0];
|
||||
|
||||
Promise.all([
|
||||
sharp(Buffer.from(lightTemplate))
|
||||
.resize({ width: size, height: size })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, "trayIconDarkTemplate.png")),
|
||||
sharp(Buffer.from(lightTemplate))
|
||||
.resize({ width: size*2, height: size*2 })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, "trayIconDarkTemplate@2x.png")),
|
||||
sharp(Buffer.from(darkTemplate))
|
||||
.resize({ width: size, height: size })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, "trayIconTemplate.png")),
|
||||
sharp(Buffer.from(darkTemplate))
|
||||
.resize({ width: size*2, height: size*2 })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, "trayIconTemplate@2x.png")),
|
||||
])
|
||||
.then((resolutions) => console.log(`Generated ${resolutions.length} images`))
|
||||
.catch(console.error);
|
||||
lightRoot.innerHTML += getSvgStyling("light");
|
||||
|
||||
return {
|
||||
light: lightRoot.outerHTML,
|
||||
dark: darkRoot.outerHTML,
|
||||
};
|
||||
}
|
||||
|
||||
async function generateNormalImages(template: string, size: number, name: string) {
|
||||
await Promise.all([
|
||||
sharp(Buffer.from(template))
|
||||
.resize({ width: size, height: size })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, `${name}.png`)),
|
||||
sharp(Buffer.from(template))
|
||||
.resize({ width: size*2, height: size*2 })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, `${name}@2x.png`)),
|
||||
]);
|
||||
}
|
||||
|
||||
async function generateUpdateAvailableImages(template: string, size: number, name: string, noticeSvg: string) {
|
||||
await Promise.all([
|
||||
sharp(Buffer.from(template))
|
||||
.composite([{
|
||||
input: (
|
||||
await sharp(Buffer.from(noticeSvg))
|
||||
.resize({
|
||||
width: Math.floor(size/1.5),
|
||||
height: Math.floor(size/1.5),
|
||||
})
|
||||
.toBuffer()
|
||||
),
|
||||
top: Math.floor(size/2.5),
|
||||
left: Math.floor(size/2.5),
|
||||
}])
|
||||
.resize({ width: size, height: size })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, `${name}.png`)),
|
||||
sharp(Buffer.from(template))
|
||||
.composite([{
|
||||
input: (
|
||||
await sharp(Buffer.from(noticeSvg))
|
||||
.resize({
|
||||
width: Math.floor((size * 2)/1.5),
|
||||
height: Math.floor((size * 2)/1.5),
|
||||
})
|
||||
.toBuffer()
|
||||
),
|
||||
top: Math.floor((size * 2)/2.5),
|
||||
left: Math.floor((size * 2)/2.5),
|
||||
}])
|
||||
.resize({ width: size*2, height: size*2 })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, `${name}@2x.png`)),
|
||||
]);
|
||||
}
|
||||
|
||||
async function getNoticeSvg(): Promise<string> {
|
||||
const svgData = await readFile(noticeFile, { encoding: "utf-8" });
|
||||
const noticeSvgRoot = new JSDOM(svgData).window.document.getElementsByTagName("svg")[0];
|
||||
|
||||
noticeSvgRoot.innerHTML += getSvgStyling("dark");
|
||||
|
||||
return noticeSvgRoot.outerHTML;
|
||||
}
|
||||
|
||||
async function generateTrayIcons() {
|
||||
try {
|
||||
console.log("Generating tray icon pngs");
|
||||
await ensureOutputFoler();
|
||||
|
||||
const baseTemplates = await getBaseIconTemplates();
|
||||
const noticeTemplate = await getNoticeSvg();
|
||||
|
||||
await Promise.all([
|
||||
generateNormalImages(baseTemplates.light, size, "trayIconDarkTemplate"),
|
||||
generateUpdateAvailableImages(baseTemplates.light, size, "trayIconDarkUpdateAvailableTemplate", noticeTemplate),
|
||||
generateNormalImages(baseTemplates.dark, size, "trayIconTemplate"),
|
||||
generateUpdateAvailableImages(baseTemplates.dark, size, "trayIconUpdateAvailableTemplate", noticeTemplate),
|
||||
]);
|
||||
|
||||
console.log("Generated all images");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
generateTrayIcons();
|
||||
|
||||
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 397 B |
|
Before Width: | Height: | Size: 724 B After Width: | Height: | Size: 717 B |
BIN
build/tray/trayIconDarkUpdateAvailableTemplate.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
build/tray/trayIconDarkUpdateAvailableTemplate@2x.png
Normal file
|
After Width: | Height: | Size: 988 B |
|
Before Width: | Height: | Size: 397 B After Width: | Height: | Size: 392 B |
|
Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 724 B |
BIN
build/tray/trayIconUpdateAvailableTemplate.png
Normal file
|
After Width: | Height: | Size: 521 B |
BIN
build/tray/trayIconUpdateAvailableTemplate@2x.png
Normal file
|
After Width: | Height: | Size: 916 B |
@ -3,17 +3,15 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { Menu, Tray } from "electron";
|
||||
import type { Menu } from "electron";
|
||||
import { 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";
|
||||
|
||||
const TRAY_LOG_PREFIX = "[TRAY]";
|
||||
|
||||
const electronTrayInjectable = getInjectable({
|
||||
id: "electron-tray",
|
||||
@ -23,13 +21,13 @@ const electronTrayInjectable = getInjectable({
|
||||
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 +39,14 @@ const electronTrayInjectable = getInjectable({
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
stop: () => {
|
||||
tray.destroy();
|
||||
},
|
||||
|
||||
setMenuItems: (items: TrayMenuItem[]) => {
|
||||
pipeline(
|
||||
items,
|
||||
convertToElectronMenuTemplate,
|
||||
Menu.buildFromTemplate,
|
||||
|
||||
(template) => {
|
||||
tray.setContextMenu(template);
|
||||
},
|
||||
);
|
||||
setMenu: (menu: Menu) => {
|
||||
tray.setContextMenu(menu);
|
||||
},
|
||||
setIconPath: (iconPath: string) => {
|
||||
tray.setImage(iconPath);
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -64,53 +55,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 reactiveMenuIconInjectable = getInjectable({
|
||||
id: "reactive-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 reactiveMenuIconInjectable;
|
||||
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 loggerInjectable from "../../../common/logger.injectable";
|
||||
import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
|
||||
|
||||
const convertToElectronMenuTemplateInjectable = getInjectable({
|
||||
id: "convert-to-electron-menu-template",
|
||||
instantiate: (di) => {
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return (trayMenuItems: TrayMenuItem[]) => {
|
||||
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: () => {
|
||||
(async () => {
|
||||
try {
|
||||
await trayMenuItem.click?.();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[TRAY]: clicking item "${trayMenuItem.id} failed."`,
|
||||
{ error },
|
||||
);
|
||||
}
|
||||
})();
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: "submenu",
|
||||
submenu: toTrayMenuOptions(trayMenuItem.id),
|
||||
}),
|
||||
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return toTrayMenuOptions(null);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default convertToElectronMenuTemplateInjectable;
|
||||
@ -4,20 +4,26 @@
|
||||
*/
|
||||
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 trayMenuInjectable from "./tray-menu.injectable";
|
||||
|
||||
const reactiveTrayMenuItemsInjectable = getInjectable({
|
||||
id: "reactive-tray-menu-items",
|
||||
|
||||
instantiate: (di) => {
|
||||
const electronTray = di.inject(electronTrayInjectable);
|
||||
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
||||
const trayMenu = di.inject(trayMenuInjectable);
|
||||
|
||||
return getStartableStoppable("reactive-tray-menu-items", () => autorun(() => {
|
||||
electronTray.setMenuItems(trayMenuItems.get());
|
||||
}));
|
||||
return getStartableStoppable("reactive-tray-menu-items", () => (
|
||||
reaction(
|
||||
() => trayMenu.get(),
|
||||
electronTray.setMenu,
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
)
|
||||
));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 { Menu } from "electron";
|
||||
import { computed } from "mobx";
|
||||
import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable";
|
||||
import convertToElectronMenuTemplateInjectable from "./convert-to-electron-menu-template.injectable";
|
||||
|
||||
const trayMenuInjectable = getInjectable({
|
||||
id: "tray-menu",
|
||||
instantiate: (di) => {
|
||||
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
||||
const convertToElectronMenuTemplate = di.inject(convertToElectronMenuTemplateInjectable);
|
||||
|
||||
return computed(() => (
|
||||
Menu.buildFromTemplate(
|
||||
convertToElectronMenuTemplate(
|
||||
trayMenuItems.get(),
|
||||
),
|
||||
)
|
||||
));
|
||||
},
|
||||
});
|
||||
|
||||
export default trayMenuInjectable;
|
||||
@ -7,20 +7,23 @@ import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injec
|
||||
import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable";
|
||||
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
||||
|
||||
const trayIconPathInjectable = getInjectable({
|
||||
id: "tray-icon-path",
|
||||
const trayIconPathsInjectable = getInjectable({
|
||||
id: "tray-icon-paths",
|
||||
|
||||
instantiate: (di) => {
|
||||
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
|
||||
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
|
||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
||||
|
||||
return getAbsolutePath(
|
||||
const baseIconDirectory = getAbsolutePath(
|
||||
staticFilesDirectory,
|
||||
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
|
||||
"trayIconTemplate.png",
|
||||
);
|
||||
|
||||
return {
|
||||
normal: getAbsolutePath(baseIconDirectory, "trayIconTemplate.png"),
|
||||
updateAvailable: getAbsolutePath(baseIconDirectory, "trayIconUpdateAvailableTemplate.png"),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@ -29,6 +29,7 @@ import Spinner from "./spinner.svg";
|
||||
import Ssh from "./ssh.svg";
|
||||
import Storage from "./storage.svg";
|
||||
import Terminal from "./terminal.svg";
|
||||
import Notice from "./notice.svg";
|
||||
import User from "./user.svg";
|
||||
import Users from "./users.svg";
|
||||
import Wheel from "./wheel.svg";
|
||||
@ -58,6 +59,7 @@ const localSvgIcons = new Map([
|
||||
["ssh", Ssh],
|
||||
["storage", Storage],
|
||||
["terminal", Terminal],
|
||||
["notice", Notice],
|
||||
["user", User],
|
||||
["users", Users],
|
||||
["wheel", Wheel],
|
||||
|
||||
30
src/renderer/components/icon/notice.svg
Normal file
@ -0,0 +1,30 @@
|
||||
<svg xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" sodipodi:docname="notice.svg" inkscape:version="1.0 (4035a4f, 2020-05-01)" id="notice" version="1.1" viewBox="0 0 135.46666 135.46666" height="512" width="512">
|
||||
<defs id="defs2">
|
||||
<rect id="rect2785" height="134.1693" width="135.23837" y="0.26726951" x="-0.53453903" />
|
||||
<linearGradient osb:paint="solid" id="linearGradient837">
|
||||
<stop id="stop835" offset="0" style="stop-color:#000000;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<sodipodi:namedview inkscape:window-maximized="0" inkscape:window-y="0" inkscape:window-x="0" inkscape:window-height="1080" inkscape:window-width="1920" inkscape:pagecheckerboard="true" units="px" showgrid="false" inkscape:document-rotation="0" inkscape:current-layer="layer1" inkscape:document-units="px" inkscape:cy="263.00246" inkscape:cx="240.74335" inkscape:zoom="2.0359339" inkscape:pageshadow="2" inkscape:pageopacity="0.0" borderopacity="1.0" bordercolor="#666666" pagecolor="#ffffff" id="base" />
|
||||
<metadata id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1">
|
||||
<ellipse ry="59.764721" rx="59.764713" cy="67.73333" cx="67.733345" id="path10" style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:15.9372;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path id="path2807" d="M 58.707956,29.522524 H 76.674473 L 76.739452,43.330427 73.23062,71.076192 62.054343,71.043701 58.740444,43.427894 Z" style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<rect y="83.204803" x="59.292759" height="14.815064" width="16.861885" id="rect2809" style="fill:#000000;fill-opacity:1;stroke:none;" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |