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
|
build: node_modules binaries/client
|
||||||
yarn run npm:fix-build-version
|
yarn run npm:fix-build-version
|
||||||
$(MAKE) build-extensions -B
|
$(MAKE) build-extensions -B
|
||||||
|
yarn run build:tray-icons
|
||||||
yarn run compile
|
yarn run compile
|
||||||
ifeq "$(DETECTED_OS)" "Windows"
|
ifeq "$(DETECTED_OS)" "Windows"
|
||||||
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
# 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.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from "fs";
|
import { ensureDir, readFile } from "fs-extra";
|
||||||
import { ensureDirSync } from "fs-extra";
|
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
@ -12,39 +11,123 @@ import sharp from "sharp";
|
|||||||
const size = Number(process.env.OUTPUT_SIZE || "16");
|
const size = Number(process.env.OUTPUT_SIZE || "16");
|
||||||
const outputFolder = process.env.OUTPUT_DIR || "./build/tray";
|
const outputFolder = process.env.OUTPUT_DIR || "./build/tray";
|
||||||
const inputFile = process.env.INPUT_SVG_PATH || "./src/renderer/components/icon/logo-lens.svg";
|
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" });
|
async function ensureOutputFoler() {
|
||||||
const svgDom = new JSDOM(`<body>${svgData}</body>`);
|
await ensureDir(outputFolder);
|
||||||
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
}
|
||||||
|
|
||||||
svgRoot.innerHTML += `<style>* {fill: white !important;}</style>`;
|
function getSvgStyling(colouring: "dark" | "light"): string {
|
||||||
const lightTemplate = svgRoot.outerHTML;
|
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([
|
lightRoot.innerHTML += getSvgStyling("light");
|
||||||
sharp(Buffer.from(lightTemplate))
|
|
||||||
.resize({ width: size, height: size })
|
return {
|
||||||
.png()
|
light: lightRoot.outerHTML,
|
||||||
.toFile(path.join(outputFolder, "trayIconDarkTemplate.png")),
|
dark: darkRoot.outerHTML,
|
||||||
sharp(Buffer.from(lightTemplate))
|
};
|
||||||
.resize({ width: size*2, height: size*2 })
|
}
|
||||||
.png()
|
|
||||||
.toFile(path.join(outputFolder, "trayIconDarkTemplate@2x.png")),
|
async function generateNormalImages(template: string, size: number, name: string) {
|
||||||
sharp(Buffer.from(darkTemplate))
|
await Promise.all([
|
||||||
.resize({ width: size, height: size })
|
sharp(Buffer.from(template))
|
||||||
.png()
|
.resize({ width: size, height: size })
|
||||||
.toFile(path.join(outputFolder, "trayIconTemplate.png")),
|
.png()
|
||||||
sharp(Buffer.from(darkTemplate))
|
.toFile(path.join(outputFolder, `${name}.png`)),
|
||||||
.resize({ width: size*2, height: size*2 })
|
sharp(Buffer.from(template))
|
||||||
.png()
|
.resize({ width: size*2, height: size*2 })
|
||||||
.toFile(path.join(outputFolder, "trayIconTemplate@2x.png")),
|
.png()
|
||||||
])
|
.toFile(path.join(outputFolder, `${name}@2x.png`)),
|
||||||
.then((resolutions) => console.log(`Generated ${resolutions.length} images`))
|
]);
|
||||||
.catch(console.error);
|
}
|
||||||
|
|
||||||
|
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.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
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 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 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 isWindowsInjectable from "../../../common/vars/is-windows.injectable";
|
||||||
import loggerInjectable from "../../../common/logger.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({
|
const electronTrayInjectable = getInjectable({
|
||||||
id: "electron-tray",
|
id: "electron-tray",
|
||||||
@ -23,13 +21,13 @@ const electronTrayInjectable = getInjectable({
|
|||||||
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
|
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
|
||||||
const isWindows = di.inject(isWindowsInjectable);
|
const isWindows = di.inject(isWindowsInjectable);
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
const trayIconPath = di.inject(trayIconPathInjectable);
|
const trayIconPaths = di.inject(trayIconPathsInjectable);
|
||||||
|
|
||||||
let tray: Tray;
|
let tray: Tray;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: () => {
|
start: () => {
|
||||||
tray = new Tray(trayIconPath);
|
tray = new Tray(trayIconPaths.normal);
|
||||||
|
|
||||||
tray.setToolTip(packageJson.description);
|
tray.setToolTip(packageJson.description);
|
||||||
tray.setIgnoreDoubleClickEvents(true);
|
tray.setIgnoreDoubleClickEvents(true);
|
||||||
@ -41,21 +39,14 @@ const electronTrayInjectable = getInjectable({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: () => {
|
stop: () => {
|
||||||
tray.destroy();
|
tray.destroy();
|
||||||
},
|
},
|
||||||
|
setMenu: (menu: Menu) => {
|
||||||
setMenuItems: (items: TrayMenuItem[]) => {
|
tray.setContextMenu(menu);
|
||||||
pipeline(
|
},
|
||||||
items,
|
setIconPath: (iconPath: string) => {
|
||||||
convertToElectronMenuTemplate,
|
tray.setImage(iconPath);
|
||||||
Menu.buildFromTemplate,
|
|
||||||
|
|
||||||
(template) => {
|
|
||||||
tray.setContextMenu(template);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -64,53 +55,3 @@ const electronTrayInjectable = getInjectable({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default electronTrayInjectable;
|
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 { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
|
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
|
||||||
import { autorun } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable";
|
|
||||||
import electronTrayInjectable from "../electron-tray/electron-tray.injectable";
|
import electronTrayInjectable from "../electron-tray/electron-tray.injectable";
|
||||||
|
import trayMenuInjectable from "./tray-menu.injectable";
|
||||||
|
|
||||||
const reactiveTrayMenuItemsInjectable = getInjectable({
|
const reactiveTrayMenuItemsInjectable = getInjectable({
|
||||||
id: "reactive-tray-menu-items",
|
id: "reactive-tray-menu-items",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const electronTray = di.inject(electronTrayInjectable);
|
const electronTray = di.inject(electronTrayInjectable);
|
||||||
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
const trayMenu = di.inject(trayMenuInjectable);
|
||||||
|
|
||||||
return getStartableStoppable("reactive-tray-menu-items", () => autorun(() => {
|
return getStartableStoppable("reactive-tray-menu-items", () => (
|
||||||
electronTray.setMenuItems(trayMenuItems.get());
|
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 staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable";
|
||||||
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
||||||
|
|
||||||
const trayIconPathInjectable = getInjectable({
|
const trayIconPathsInjectable = getInjectable({
|
||||||
id: "tray-icon-path",
|
id: "tray-icon-paths",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
|
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
|
||||||
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
|
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
|
||||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
const isDevelopment = di.inject(isDevelopmentInjectable);
|
||||||
|
const baseIconDirectory = getAbsolutePath(
|
||||||
return getAbsolutePath(
|
|
||||||
staticFilesDirectory,
|
staticFilesDirectory,
|
||||||
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
|
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) => {
|
instantiate: (di) => {
|
||||||
const extensions = di.inject(mainExtensionsInjectable);
|
const extensions = di.inject(mainExtensionsInjectable);
|
||||||
|
|
||||||
return computed(() =>
|
return computed(() => extensions.get().flatMap(extension => extension.trayMenus));
|
||||||
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 Ssh from "./ssh.svg";
|
||||||
import Storage from "./storage.svg";
|
import Storage from "./storage.svg";
|
||||||
import Terminal from "./terminal.svg";
|
import Terminal from "./terminal.svg";
|
||||||
|
import Notice from "./notice.svg";
|
||||||
import User from "./user.svg";
|
import User from "./user.svg";
|
||||||
import Users from "./users.svg";
|
import Users from "./users.svg";
|
||||||
import Wheel from "./wheel.svg";
|
import Wheel from "./wheel.svg";
|
||||||
@ -58,6 +59,7 @@ const localSvgIcons = new Map([
|
|||||||
["ssh", Ssh],
|
["ssh", Ssh],
|
||||||
["storage", Storage],
|
["storage", Storage],
|
||||||
["terminal", Terminal],
|
["terminal", Terminal],
|
||||||
|
["notice", Notice],
|
||||||
["user", User],
|
["user", User],
|
||||||
["users", Users],
|
["users", Users],
|
||||||
["wheel", Wheel],
|
["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 |