From 9ebd49561772c2a397ccfa1fff81324d53b8ebe5 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 6 Jun 2022 09:51:56 -0400 Subject: [PATCH] Add reactive tray icon for when update is available Signed-off-by: Sebastian Malton --- Makefile | 1 + build/generate-tray-icons.ts | 145 ++++++++++++++---- build/tray/trayIconDarkTemplate.png | Bin 392 -> 397 bytes build/tray/trayIconDarkTemplate@2x.png | Bin 724 -> 717 bytes .../trayIconDarkUpdateAvailableTemplate.png | Bin 0 -> 533 bytes ...trayIconDarkUpdateAvailableTemplate@2x.png | Bin 0 -> 988 bytes build/tray/trayIconTemplate.png | Bin 397 -> 392 bytes build/tray/trayIconTemplate@2x.png | Bin 717 -> 724 bytes .../tray/trayIconUpdateAvailableTemplate.png | Bin 0 -> 521 bytes .../trayIconUpdateAvailableTemplate@2x.png | Bin 0 -> 916 bytes .../electron-tray/electron-tray.injectable.ts | 83 ++-------- .../reactive-menu-icon.injectable.ts | 37 +++++ ...rt-to-electron-menu-template.injectable.ts | 63 ++++++++ .../reactive-tray-menu-items.injectable.ts | 18 ++- .../tray-menu.injectable.ts | 27 ++++ src/main/tray/tray-icon-path.injectable.ts | 15 +- src/main/tray/tray-menu-items.injectable.ts | 3 +- src/main/tray/tray.ts | 99 ------------ src/renderer/components/icon/icon.tsx | 2 + src/renderer/components/icon/notice.svg | 30 ++++ 20 files changed, 308 insertions(+), 215 deletions(-) create mode 100644 build/tray/trayIconDarkUpdateAvailableTemplate.png create mode 100644 build/tray/trayIconDarkUpdateAvailableTemplate@2x.png create mode 100644 build/tray/trayIconUpdateAvailableTemplate.png create mode 100644 build/tray/trayIconUpdateAvailableTemplate@2x.png create mode 100644 src/main/tray/reactive-menu-icon/reactive-menu-icon.injectable.ts create mode 100644 src/main/tray/reactive-tray-menu-items/convert-to-electron-menu-template.injectable.ts create mode 100644 src/main/tray/reactive-tray-menu-items/tray-menu.injectable.ts delete mode 100644 src/main/tray/tray.ts create mode 100644 src/renderer/components/icon/notice.svg 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 63f2eb18953367d8687dcef3a423acadc3af84a1..0e1c5d6e8ee2cb89291ed7d236c45af8ea9491d8 100644 GIT binary patch delta 349 zcmV-j0iynh1C0ZaIe$M%L_t(IjeXOxN?cJ82H?*^yMU`cL5d)Vh1l2%`vhVw#7YoE zG{Gkj#7+c7thQG~TOUA61uYGvT8qTis1Rjfu3oQa9hl{wGxN`$nfZUpHjG*Pz%nL! zD|3q0hxHn;@@-7vf1YnJ+5`{b5QDg_fq(BXg7Zev--&s(y$sA_ zp_aIaxx%Z$cPelTk9pb6^98Qaj}A@?eu4ox?Rw=+Or!nwKwq#{@bL)#Y!rwMeDl$c z3T>>)pJw2=P~a=>8+j`cn8ZfG>r-xLTN{l6Gjb3oJqFduL0;i9*W9hO+N%Cxx5uht zg-f+_toVC=2UbR9QTbN`e3otesonieVjYwy9rO&EMqU{zAES*v@cpz4Z(zP vLO>y;3ivM>`;Nk&Sx7jf9BYng7ghKI;f#FQ*R~j600000NkvXXu0mjfK>VhK delta 344 zcmV-e0jK_r1Be5VIe$7yL_t(IjeV27N&`_4g*S!igv9s+79xm+*w_Yqf?zFZB?v)` z;u8pBCxSt&(kr5^51^%iSVpjlh1&WDesW;e3^%hmaAEKK-JO|pvMc~l16F}uU>jJB z;))W_fiH&m0p`tqteB$Y5^%sMPJm_L*Y15_E=`^SE`bX0;(wCgfob4ADf+A&U*OCf z8^ERu+yd5!ahfMrfp0So?EU~e0uw+7xDCz?P|j@H9!EeOSaE+i3G4@FLC(FTfHp82 z4EZW(qa{Zxa9jrkW`K_*ZbyzK;4nD!5Vj0Fwph7lFB6;s*USg=Y@(7r=b!+^&$j=5^vM(5c^> z91d)i>~@E2UyQs1_zAoL8nt(`!r-kSLtZ+cYgZY0V=mxMXnzIpt-z4I^=ipuf17k>G%>Q+e`vz(|4M6`eOJrNXbsiq54(@=QMq+$n4< z5&Q+L|0BZQV1J+X76E5~b9JocvN46;3U41PHRk}2fTt-r?Zv&wmF44ds#utZzy|o8z%AlA%>!Q>6;eSvtnU(l(+F|c=s4(Wdwi`|d zEIZysYz>794-yHqd(?=3oI4LkE?&#}mTDI0}W4x7JE3bClSs8Ww_wgGzMD|jpj3@a30000vlpIfgyc>WV8x$ z6)merQ5m!d)QCWAC9b4JNaQZu7WM%_QAAK!L_wv|q7RBJs9&7td2pKU%sun0g?|{{ zaPR+o%$xf^=S-3;6#y%NZs0s{3wQ~91?GWwzzv}3e*|s4 z8+c6|AAn}H88+*0fW5T_@4}BGYSRw<2<%@3>b!xM>u(+K&=LFzP*R(z9NvU031ATD z)%$IZ1a>HQw}U$p3%(2Z3A_O6b!{@o;WeX>r}(fhD|mhC!1e3;df;2Yk-Y0o$|>WA zNVYMQKsiMejeob9*OJBs-D?3pQVeI(!B+sY0pcayU#*g|Pf;m4z*xcHRr``nU~2~P z7qCS+eP;cjt~CGyz_}W(bv6MhuUWV!(gc@TFZwpTfCL~4Yx;~vu%hGyfj3Ae?JEUJ!mFl=8@Eb0!S7KvY;#U9w N002ovPDHLkV1iY|JzD?( diff --git a/build/tray/trayIconDarkUpdateAvailableTemplate.png b/build/tray/trayIconDarkUpdateAvailableTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..ced61d695c93523c94505a9f73da9dd8accb76d9 GIT binary patch literal 533 zcmV+w0_y#VP);rp1@jr2y#J4~M7>hNqWA|U!m`pndbW_9xFM(UY#`<4@>n`~+a0FZgex%5A z;3_b2L{BYeYj5nqBj9l=a2I%(QI+s2AzuQ{?B;jte+Iq)=Ye;?Nd}IA3)pF=m7fE* zfjc@h#@t^l79WEkSPRqX^job~8vzHv4aGcl1oQ{4=^zZlK@bFIp66{Um5LEO&oj&A z(sVi<#XiA)-2UDhWA2GuC=}woe!p*eyc2Jsvczuj(ceBb}@_bUClp1P(A*o?>HeYK&1X)mA8$I!Y| zDw)-4wKm4wO$*5MQ1oE*aCW-g?k}~X{#CM|%pylo^vor@rYF2tuh$PXZS^at>~!Ye XHRvMj2KpK%00000NkvXXu0mjfzeen% literal 0 HcmV?d00001 diff --git a/build/tray/trayIconDarkUpdateAvailableTemplate@2x.png b/build/tray/trayIconDarkUpdateAvailableTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..20c0cfc2d8e5177643553d26a6ce6227564f91ac GIT binary patch literal 988 zcmV<210(#2P)UUn zm4T~tVNp06D`E(8MiiG)9{_#<_y!;bp!Qz`t_5%tKpFr!=$HY& z1mHG+-CGUb4&aMRVAd4?9%=K?FrET%akIfsYXnw$UE25ntXj;60c_U|Tp_<~@J=K6 zdjLD6&2tCbQ(Y4G09=s&r!)whk=U~u*pORrKY-r=J^`qbW0MXJpBXCTgXVEvvI|~i zO1N8cd_RDn7LNF=4@*pc8TrB`TVq9_-vs0u-)lG)v!e%c&I=%01_9Q9Id~<2uNJ_^ za=u!YlyMcR={zbkxUyG?E99gNa2>#jzYtt8j9-;wy8zq;aDNlJPx=rNbYS~dmgY79 zaR3Rcaw4LmM0AIU;zX1tqE-2g%kd*{kQXjnc3I+%%iS^tK14**q#;vNQ`FSdqhEwnj5EGZYGiXmD^)d}n88#ki7OE=Qf6oyzBieAh@EK5KLE zf(kr5JWO7%*V2~9nT#S*C=|%=_j4IBRy zy1I(S+uGU$UfQh_k?;pu7CPlKAqh;YmB{TZ(o`xXf-EmDm&`F)!;wrX0&jF{cwk`Q znzH6?x598>I|8gDBO}ViI?5GUQeqW$XTGqoAiU4d&s)yd*Vl{PV^Mzh0Jx!}$z(Fg zhS6x$c9*!^SjNZ4Z839mb43IyZCY`OW-=Ma?PujVmS8YwiwTFrMFb!(2v-96d|ns^ z0s&i0UtgcV^!E1JV%XkHphl+}(rNClBah(zoCOPM37>{#ncg_Ki$3u;cjZ{}x zr=0^v;b)Vr;n2{~M%(3Vw!$P534vK$T(q2TXlN*@0;BLv_r#7sOH0d3Rm673;6`9{ zbkwyERG8lPO1Xdg_+oSFgg&2-nwy)&*QFCi;3DTu??fhrNF;JBm&Z%LnR`;Tbl@D_7^pE8?uS%LzVn-gbqTx2Sr~qg-JUQNGXpet%xx zY8%&VEEW^F!FA4G&UQm!v1wZgULt&@@~y@RRrLHR6`r3kZ~GrsdyA+;yV7U?0000< KMNUMnLSTa5YPskD literal 0 HcmV?d00001 diff --git a/build/tray/trayIconTemplate.png b/build/tray/trayIconTemplate.png index 0e1c5d6e8ee2cb89291ed7d236c45af8ea9491d8..63f2eb18953367d8687dcef3a423acadc3af84a1 100644 GIT binary patch delta 344 zcmV-e0jK_r1Be5VIe$7yL_t(IjeV27N&`_4g*S!igv9s+79xm+*w_Yqf?zFZB?v)` z;u8pBCxSt&(kr5^51^%iSVpjlh1&WDesW;e3^%hmaAEKK-JO|pvMc~l16F}uU>jJB z;))W_fiH&m0p`tqteB$Y5^%sMPJm_L*Y15_E=`^SE`bX0;(wCgfob4ADf+A&U*OCf z8^ERu+yd5!ahfMrfp0So?EU~e0uw+7xDCz?P|j@H9!EeOSaE+i3G4@FLC(FTfHp82 z4EZW(qa{Zxa9jrkW`K_*ZbyzK;4nDD|3q0hxHn;@@-7vf1YnJ+5`{b5QDg_fq(BXg7Zev--&s(y$sA_ zp_aIaxx%Z$cPelTk9pb6^98Qaj}A@?eu4ox?Rw=+Or!nwKwq#{@bL)#Y!rwMeDl$c z3T>>)pJw2=P~a=>8+j`cn8ZfG>r-xLTN{l6Gjb3oJqFduL0;i9*W9hO+N%Cxx5uht zg-f+_toVC=2UbR9QTbN`e3otesonieVjYwy9rO&EMqU{zAES*v@cpz4Z(zP vLO>y;3ivM>`;Nk&Sx7jf9BYng7ghKI;f#FQ*R~j600000NkvXXu0mjfK>VhK diff --git a/build/tray/trayIconTemplate@2x.png b/build/tray/trayIconTemplate@2x.png index 553a8ec373fdc3b82c797bf91c69a242ab811f06..c5dcfa9e15650ceec069192748547dbfc865142e 100644 GIT binary patch delta 679 zcmV;Y0$Bac1=IzQIe&&pL_t(oh1Hi$NEJ~O$EWO-LWvs<7Y0F*>vlpIfgyc>WV8x$ z6)merQ5m!d)QCWAC9b4JNaQZu7WM%_QAAK!L_wv|q7RBJs9&7td2pKU%sun0g?|{{ zaPR+o%$xf^=S-3;6#y%NZs0s{3wQ~91?GWwzzv}3e*|s4 z8+c6|AAn}H88+*0fW5T_@4}BGYSRw<2<%@3>b!xM>u(+K&=LFzP*R(z9NvU031ATD z)%$IZ1a>HQw}U$p3%(2Z3A_O6b!{@o;WeX>r}(fhD|mhC!1e3;df;2Yk-Y0o$|>WA zNVYMQKsiMejeob9*OJBs-D?3pQVeI(!B+sY0pcayU#*g|Pf;m4z*xcHRr``nU~2~P z7qCS+eP;cjt~CGyz_}W(bv6MhuUWV!(gc@TFZwpTfCL~4Yx;~vu%hGyfj3Ae?JEUJ!mFl=8@Eb0!S7KvY;#U9w N002ovPDHLkV1iY|JzD?( delta 672 zcmV;R0$=^q1!5Vj0Fwph7lFB6;s*USg=Y@(7r=b!+^&$j=5^vM(5c^> z91d)i>~@E2UyQs1_zAoL8nt(`!r-kSLtZ+cYgZY0V=mxMXnzIpt-z4I^=ipuf17k>G%>Q+e`vz(|4M6`eOJrNXbsiq54(@=QMq+$n4< z5&Q+L|0BZQV1J+X76E5~b9JocvN46;3U41PHRk}2fTt-r?Zv&wmF44ds#utZzy|o8z%AlA%>!Q>6;eSvtnU(l(+F|c=s4(Wdwi`|d zEIZysYz>794-yHqd(?=3oI4LkE?&#}mTDI0}W4x7JE3bClSs8Ww_wgGzMD|jpj3@a30000 z_m)Zp0XzVdbg6q13VMrY7 za%f4QRnXrDU_<0;wTcch7!2t5`^nmIoJ0gTlE5l3$iwrzFWOlIVLTo)8jX^*(Cv2d zeg9kn3)YZ-Oftq?)$8>+K}T=5+w)xQL{T(JZ(={+gE1y4N(tq1IYF8?n@tIXsem5P zx2!-A1ixL^owqO?4ig{Jyw~f^eBVFKKBi^+Oci>zJ7E}J$uRw{R4NIyuGMNXQP;3u zG)x*84bDm&$A9Fu4AYSX?JRN>MX&!inT{RhBe`vi3DvWXo!$8_WZ9zuo<^wE00000 LNkvXXu0mjf-4Wye literal 0 HcmV?d00001 diff --git a/build/tray/trayIconUpdateAvailableTemplate@2x.png b/build/tray/trayIconUpdateAvailableTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5ea256b7380843677651ebd08902f2d053f9d27d GIT binary patch literal 916 zcmV;F18e+=P)-?``9dtWH@zW~?(v;a}y5%3oH3Va7X0V$yJUpTG=E&+=KW5nRU04@WY z|JHd6@Sgnm3{>km14jP>aH?46hq1>6Jtqt-d5p(_5?jaX^|J(cZsGhTP^RbH58&Ol z5&>KVPU_!>ECd|U++!BpSU%@RfuFz|pj`Kj2N=9(RLCpraN5s#xhvs%b^mtYn};D; z>)o2ugFU2VE4&2sxQJ5YTa9ILU=GknPwzjr4Z@jRuQnB;%^J~ULtPzQE zN5B*8ao+hMVfPBTIQA1DZD?p{H433Zp-W1=h2NUb%*-h3)6>(Qj;o;%neSnqY*Sz05SkhPT zM9@wc4u@%MY*f!YJK-Hq0fvwhu6yEu?%${Pa<#Y19&eL)t*xzOG8yw@W=Eg^<2u%> zKewD~*oZqHB&+>$Le^Zu?yS2$Mb&jaR4DYfUsB%@yf*SOSM;$oQB$?7sml%3ai7Y-hdz0000 { - 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 + + + + + + + + + + +