diff --git a/Makefile b/Makefile index 48ce768766..9dfb2cf512 100644 --- a/Makefile +++ b/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 diff --git a/build/generate-tray-icons.ts b/build/generate-tray-icons.ts index a7ab3bd48b..c8ca4f1831 100644 --- a/build/generate-tray-icons.ts +++ b/build/generate-tray-icons.ts @@ -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(`${svgData}`); -const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0]; +async function ensureOutputFoler() { + await ensureDir(outputFolder); +} -svgRoot.innerHTML += ``; -const lightTemplate = svgRoot.outerHTML; +function getSvgStyling(colouring: "dark" | "light"): string { + return ` + + `; +} -svgRoot.innerHTML += ``; +async function getBaseIconTemplates() { + const svgData = await readFile(inputFile, { encoding: "utf-8" }); -const darkTemplate = svgRoot.outerHTML; + const darkDom = new JSDOM(`${svgData}`); + 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(`${svgData}`); + 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 { + 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(); diff --git a/build/tray/trayIconDarkTemplate.png b/build/tray/trayIconDarkTemplate.png index 63f2eb1895..0e1c5d6e8e 100644 Binary files a/build/tray/trayIconDarkTemplate.png and b/build/tray/trayIconDarkTemplate.png differ diff --git a/build/tray/trayIconDarkTemplate@2x.png b/build/tray/trayIconDarkTemplate@2x.png index c5dcfa9e15..553a8ec373 100644 Binary files a/build/tray/trayIconDarkTemplate@2x.png and b/build/tray/trayIconDarkTemplate@2x.png differ diff --git a/build/tray/trayIconDarkUpdateAvailableTemplate.png b/build/tray/trayIconDarkUpdateAvailableTemplate.png new file mode 100644 index 0000000000..ced61d695c Binary files /dev/null and b/build/tray/trayIconDarkUpdateAvailableTemplate.png differ diff --git a/build/tray/trayIconDarkUpdateAvailableTemplate@2x.png b/build/tray/trayIconDarkUpdateAvailableTemplate@2x.png new file mode 100644 index 0000000000..20c0cfc2d8 Binary files /dev/null and b/build/tray/trayIconDarkUpdateAvailableTemplate@2x.png differ diff --git a/build/tray/trayIconTemplate.png b/build/tray/trayIconTemplate.png index 0e1c5d6e8e..63f2eb1895 100644 Binary files a/build/tray/trayIconTemplate.png and b/build/tray/trayIconTemplate.png differ diff --git a/build/tray/trayIconTemplate@2x.png b/build/tray/trayIconTemplate@2x.png index 553a8ec373..c5dcfa9e15 100644 Binary files a/build/tray/trayIconTemplate@2x.png and b/build/tray/trayIconTemplate@2x.png differ diff --git a/build/tray/trayIconUpdateAvailableTemplate.png b/build/tray/trayIconUpdateAvailableTemplate.png new file mode 100644 index 0000000000..774fd08d72 Binary files /dev/null and b/build/tray/trayIconUpdateAvailableTemplate.png differ diff --git a/build/tray/trayIconUpdateAvailableTemplate@2x.png b/build/tray/trayIconUpdateAvailableTemplate@2x.png new file mode 100644 index 0000000000..5ea256b738 Binary files /dev/null and b/build/tray/trayIconUpdateAvailableTemplate@2x.png differ diff --git a/src/main/tray/electron-tray/electron-tray.injectable.ts b/src/main/tray/electron-tray/electron-tray.injectable.ts index 409e7abf3f..9da05793ab 100644 --- a/src/main/tray/electron-tray/electron-tray.injectable.ts +++ b/src/main/tray/electron-tray/electron-tray.injectable.ts @@ -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); -}; diff --git a/src/main/tray/reactive-menu-icon/reactive-menu-icon.injectable.ts b/src/main/tray/reactive-menu-icon/reactive-menu-icon.injectable.ts new file mode 100644 index 0000000000..03c7c703fc --- /dev/null +++ b/src/main/tray/reactive-menu-icon/reactive-menu-icon.injectable.ts @@ -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; diff --git a/src/main/tray/reactive-tray-menu-items/convert-to-electron-menu-template.injectable.ts b/src/main/tray/reactive-tray-menu-items/convert-to-electron-menu-template.injectable.ts new file mode 100644 index 0000000000..87407d6859 --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/convert-to-electron-menu-template.injectable.ts @@ -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; diff --git a/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts index b11654393a..ca705d23b4 100644 --- a/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts +++ b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts @@ -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, + }, + ) + )); }, }); diff --git a/src/main/tray/reactive-tray-menu-items/tray-menu.injectable.ts b/src/main/tray/reactive-tray-menu-items/tray-menu.injectable.ts new file mode 100644 index 0000000000..8bf29f9941 --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/tray-menu.injectable.ts @@ -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; diff --git a/src/main/tray/tray-icon-path.injectable.ts b/src/main/tray/tray-icon-path.injectable.ts index 1eb4d13118..c7df718d12 100644 --- a/src/main/tray/tray-icon-path.injectable.ts +++ b/src/main/tray/tray-icon-path.injectable.ts @@ -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; diff --git a/src/main/tray/tray-menu-items.injectable.ts b/src/main/tray/tray-menu-items.injectable.ts index 8ee9d25e5e..c008c123e0 100644 --- a/src/main/tray/tray-menu-items.injectable.ts +++ b/src/main/tray/tray-menu-items.injectable.ts @@ -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)); }, }); diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts deleted file mode 100644 index 4d7e39c344..0000000000 --- a/src/main/tray/tray.ts +++ /dev/null @@ -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, - showApplicationWindow: () => Promise, - 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); -}; - diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx index 084b75d804..7655194886 100644 --- a/src/renderer/components/icon/icon.tsx +++ b/src/renderer/components/icon/icon.tsx @@ -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], diff --git a/src/renderer/components/icon/notice.svg b/src/renderer/components/icon/notice.svg new file mode 100644 index 0000000000..2774b185f6 --- /dev/null +++ b/src/renderer/components/icon/notice.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + +