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

Compute Tray Icon on the fly instead of at build (#5157)

This commit is contained in:
Sebastian Malton 2022-04-28 09:31:57 -07:00 committed by GitHub
parent 51a93b8cc0
commit 8ada3c7505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 84 additions and 77 deletions

View File

@ -1,55 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import sharp from "sharp";
import jsdom from "jsdom";
import fs from "fs-extra";
export async function generateTrayIcon(
{
outputFilename = "trayIcon",
svgIconPath = path.resolve(__dirname, "../src/renderer/components/icon/logo-lens.svg"),
outputFolder = path.resolve(__dirname, "./tray"),
dpiSuffix = "2x",
pixelSize = 32,
shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors
} = {}) {
outputFilename += `${shouldUseDarkColors ? "Dark" : ""}Template`; // e.g. output trayIconDarkTemplate@2x.png
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
try {
// Modify .SVG colors
const trayIconColor = shouldUseDarkColors ? "black" : "white";
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
// Resize and convert to .PNG
const pngIconBuffer: Buffer = await sharp(svgIconBuffer)
.resize({ width: pixelSize, height: pixelSize })
.png()
.toBuffer();
// Save icon
await fs.writeFile(pngIconDestPath, pngIconBuffer);
console.info(`[DONE]: Tray icon saved at "${pngIconDestPath}"`);
} catch (err) {
console.error(`[ERROR]: ${err}`);
}
}
// Run
const iconSizes: Record<string, number> = {
"1x": 16,
"2x": 32,
"3x": 48,
};
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -124,6 +124,9 @@
"rpm", "rpm",
"AppImage" "AppImage"
], ],
"asarUnpack": [
"**/node_modules/sharp/**"
],
"extraResources": [ "extraResources": [
{ {
"from": "binaries/client/linux/${arch}/kubectl", "from": "binaries/client/linux/${arch}/kubectl",
@ -260,6 +263,7 @@
"rfc6902": "^4.0.2", "rfc6902": "^4.0.2",
"selfsigned": "^2.0.1", "selfsigned": "^2.0.1",
"semver": "^7.3.7", "semver": "^7.3.7",
"sharp": "^0.30.3",
"shell-env": "^3.0.1", "shell-env": "^3.0.1",
"spdy": "^4.0.2", "spdy": "^4.0.2",
"tar": "^6.1.11", "tar": "^6.1.11",
@ -388,7 +392,6 @@
"react-window": "^1.8.6", "react-window": "^1.8.6",
"sass": "^1.49.11", "sass": "^1.49.11",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sharp": "^0.30.3",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"tailwindcss": "^3.0.23", "tailwindcss": "^3.0.23",
"tar-stream": "^2.2.0", "tar-stream": "^2.2.0",

View File

@ -48,6 +48,17 @@ export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V):
return map.get(key); return map.get(key);
} }
/**
* Like {@link getOrInsertWith} but the builder is async and will be awaited before inserting into the map
*/
export async function getOrInsertWithAsync<K, V>(map: Map<K, V>, key: K, asyncBuilder: () => Promise<V>): Promise<V> {
if (!map.has(key)) {
map.set(key, await asyncBuilder());
}
return map.get(key);
}
/** /**
* Set the value associated with `key` iff there was not a previous value * Set the value associated with `key` iff there was not a previous value
* @param map The map to interact with * @param map The map to interact with

View File

@ -305,7 +305,7 @@ async function main(di: DiContainer) {
onQuitCleanup.push( onQuitCleanup.push(
initMenu(applicationMenuItems), initMenu(applicationMenuItems),
initTray(windowManager, trayMenuItems, navigateToPreferences), await initTray(windowManager, trayMenuItems, navigateToPreferences),
() => ShellSession.cleanup(), () => ShellSession.cleanup(),
); );

View File

@ -3,19 +3,23 @@
* 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 path from "path";
import packageInfo from "../../../package.json"; import packageInfo from "../../../package.json";
import { Menu, Tray } from "electron"; import type { NativeImage } from "electron";
import { Menu, nativeImage, nativeTheme, Tray } from "electron";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { autorun } from "mobx"; import { autorun } from "mobx";
import { showAbout } from "../menu/menu"; import { showAbout } from "../menu/menu";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater"; import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import type { WindowManager } from "../window-manager"; import type { WindowManager } from "../window-manager";
import logger from "../logger"; import logger from "../logger";
import { isDevelopment, isWindows, productName, staticFilesDirectory } from "../../common/vars"; import { isWindows, productName } from "../../common/vars";
import { exitApp } from "../exit-app"; import { exitApp } from "../exit-app";
import { toJS } from "../../common/utils"; import type { Disposer } from "../../common/utils";
import { base64, disposer, getOrInsertWithAsync, toJS } from "../../common/utils";
import type { TrayMenuRegistration } from "./tray-menu-registration"; import type { TrayMenuRegistration } from "./tray-menu-registration";
import sharp from "sharp";
import LogoLens from "../../renderer/components/icon/logo-lens.svg";
import { JSDOM } from "jsdom";
const TRAY_LOG_PREFIX = "[TRAY]"; const TRAY_LOG_PREFIX = "[TRAY]";
@ -23,25 +27,69 @@ const TRAY_LOG_PREFIX = "[TRAY]";
// note: instance of Tray should be saved somewhere, otherwise it disappears // note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray; export let tray: Tray;
export function getTrayIcon(): string { interface CreateTrayIconArgs {
return path.resolve( shouldUseDarkColors: boolean;
staticFilesDirectory, size: number;
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras sourceSvg: string;
"trayIconTemplate.png",
);
} }
export function initTray( 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, windowManager: WindowManager,
trayMenuItems: IComputedValue<TrayMenuRegistration[]>, trayMenuItems: IComputedValue<TrayMenuRegistration[]>,
navigateToPreferences: () => void, navigateToPreferences: () => void,
) { ): Promise<Disposer> {
const icon = getTrayIcon(); const icon = await createCurrentTrayIcon();
const dispose = disposer();
tray = new Tray(icon); tray = new Tray(icon);
tray.setToolTip(packageInfo.description); tray.setToolTip(packageInfo.description);
tray.setIgnoreDoubleClickEvents(true); tray.setIgnoreDoubleClickEvents(true);
dispose.push(watchShouldUseDarkColors(tray));
if (isWindows) { if (isWindows) {
tray.on("click", () => { tray.on("click", () => {
windowManager windowManager
@ -50,7 +98,7 @@ export function initTray(
}); });
} }
const disposers = [ dispose.push(
autorun(() => { autorun(() => {
try { try {
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences); const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences);
@ -60,13 +108,13 @@ export function initTray(
logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error }); logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error });
} }
}), }),
]; () => {
return () => {
disposers.forEach(disposer => disposer());
tray?.destroy(); tray?.destroy();
tray = null; tray = null;
}; },
);
return dispose;
} }
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions { function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions {