From 2e7f2d94bd7cf01438c50cfa974d9e0d071d30cd Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 8 Feb 2023 09:30:38 +0100 Subject: [PATCH 1/2] feat: icon for cloud sketch in File > Open Recent Ref: #1826 Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 4 + .../contributions/open-recent-sketch.ts | 86 +++++++++-------- .../src/browser/native-image-cache.ts | 91 ++++++++++++++++++ .../theia/core/electron-main-menu-factory.ts | 28 ++++++ .../src/node/arduino-ide-backend-module.ts | 4 + .../src/node/native-image-data-provider.ts | 61 ++++++++++++ .../src/node/static/icons/cloud.png | Bin 0 -> 9457 bytes 7 files changed, 236 insertions(+), 38 deletions(-) create mode 100644 arduino-ide-extension/src/browser/native-image-cache.ts create mode 100644 arduino-ide-extension/src/node/native-image-data-provider.ts create mode 100644 arduino-ide-extension/src/node/static/icons/cloud.png diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 6c6b44a63..bd9a8b5b8 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -356,6 +356,7 @@ import { Account } from './contributions/account'; import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget'; import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget'; import { CreateCloudCopy } from './contributions/create-cloud-copy'; +import { NativeImageCache } from './native-image-cache'; export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -1034,4 +1035,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FrontendApplicationContribution).toService(DaemonPort); bind(IsOnline).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(IsOnline); + // manages native images for the electron menu icons + bind(NativeImageCache).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(NativeImageCache); }); diff --git a/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts index a14d6a541..c17fd79a4 100644 --- a/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts @@ -1,57 +1,49 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; -import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol'; +import { NativeImage } from '@theia/core/electron-shared/electron'; import { Disposable, DisposableCollection, } from '@theia/core/lib/common/disposable'; -import { - SketchContribution, - CommandRegistry, - MenuModelRegistry, - Sketch, -} from './contribution'; +import { MenuAction } from '@theia/core/lib/common/menu'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { SketchesError } from '../../common/protocol'; +import { ConfigServiceClient } from '../config/config-service-client'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { MainMenuManager } from '../../common/main-menu-manager'; -import { OpenSketch } from './open-sketch'; +import { NativeImageCache } from '../native-image-cache'; import { NotificationCenter } from '../notification-center'; -import { nls } from '@theia/core/lib/common'; -import { SketchesError } from '../../common/protocol'; +import { CloudSketchContribution } from './cloud-contribution'; +import { CommandRegistry, MenuModelRegistry, Sketch } from './contribution'; +import { OpenSketch } from './open-sketch'; @injectable() -export class OpenRecentSketch extends SketchContribution { +export class OpenRecentSketch extends CloudSketchContribution { @inject(CommandRegistry) - protected readonly commandRegistry: CommandRegistry; - + private readonly commandRegistry: CommandRegistry; @inject(MenuModelRegistry) - protected readonly menuRegistry: MenuModelRegistry; - - @inject(MainMenuManager) - protected readonly mainMenuManager: MainMenuManager; - - @inject(WorkspaceServer) - protected readonly workspaceServer: WorkspaceServer; - + private readonly menuRegistry: MenuModelRegistry; @inject(NotificationCenter) - protected readonly notificationCenter: NotificationCenter; + private readonly notificationCenter: NotificationCenter; + @inject(NativeImageCache) + private readonly imageCache: NativeImageCache; + @inject(ConfigServiceClient) + private readonly configServiceClient: ConfigServiceClient; - protected toDispose = new DisposableCollection(); + private readonly toDispose = new DisposableCollection(); + private cloudImage: NativeImage | undefined; override onStart(): void { this.notificationCenter.onRecentSketchesDidChange(({ sketches }) => this.refreshMenu(sketches) ); + this.imageCache + .getImage('cloud') + .then((image) => (this.cloudImage = image)); } override async onReady(): Promise { this.update(); } - private update(forceUpdate?: boolean): void { - this.sketchesService - .recentlyOpenedSketches(forceUpdate) - .then((sketches) => this.refreshMenu(sketches)); - } - override registerMenus(registry: MenuModelRegistry): void { registry.registerSubmenu( ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, @@ -60,12 +52,18 @@ export class OpenRecentSketch extends SketchContribution { ); } + private update(forceUpdate?: boolean): void { + this.sketchesService + .recentlyOpenedSketches(forceUpdate) + .then((sketches) => this.refreshMenu(sketches)); + } + private refreshMenu(sketches: Sketch[]): void { this.register(sketches); - this.mainMenuManager.update(); + this.menuManager.update(); } - protected register(sketches: Sketch[]): void { + private register(sketches: Sketch[]): void { const order = 0; this.toDispose.dispose(); for (const sketch of sketches) { @@ -88,13 +86,14 @@ export class OpenRecentSketch extends SketchContribution { }, }; this.commandRegistry.registerCommand(command, handler); + const menuAction = this.assignImage(sketch, { + commandId: command.id, + label: sketch.name, + order: String(order), + }); this.menuRegistry.registerMenuAction( ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, - { - commandId: command.id, - label: sketch.name, - order: String(order), - } + menuAction ); this.toDispose.pushAll([ new DisposableCollection( @@ -108,4 +107,15 @@ export class OpenRecentSketch extends SketchContribution { ]); } } + + private assignImage(sketch: Sketch, menuAction: MenuAction): MenuAction { + if (this.cloudImage) { + const dataDirUri = this.configServiceClient.tryGetDataDirUri(); + const isCloud = this.createFeatures.isCloud(sketch, dataDirUri); + if (isCloud) { + Object.assign(menuAction, { nativeImage: this.cloudImage }); + } + } + return menuAction; + } } diff --git a/arduino-ide-extension/src/browser/native-image-cache.ts b/arduino-ide-extension/src/browser/native-image-cache.ts new file mode 100644 index 000000000..268a96ae7 --- /dev/null +++ b/arduino-ide-extension/src/browser/native-image-cache.ts @@ -0,0 +1,91 @@ +import { + NativeImage, + nativeImage, + Size, +} from '@theia/core/electron-shared/electron'; +import { Endpoint } from '@theia/core/lib/browser/endpoint'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { injectable } from '@theia/core/shared/inversify'; +import fetch from 'cross-fetch'; + +const nativeImageIdentifierLiterals = ['cloud'] as const; +export type NativeImageIdentifier = + typeof nativeImageIdentifierLiterals[number]; +export const nativeImages: Record = { + cloud: 'cloud.png', +}; + +@injectable() +export class NativeImageCache implements FrontendApplicationContribution { + private readonly cache = new Map(); + private readonly loading = new Map< + NativeImageIdentifier, + Promise + >(); + + onStart(): void { + Object.keys(nativeImages).forEach((identifier: NativeImageIdentifier) => + this.getImage(identifier) + ); + } + + tryGetImage(identifier: NativeImageIdentifier): NativeImage | undefined { + return this.cache.get(identifier); + } + + async getImage(identifier: NativeImageIdentifier): Promise { + const image = this.cache.get(identifier); + if (image) { + return image; + } + let loading = this.loading.get(identifier); + if (!loading) { + const deferred = new Deferred(); + loading = deferred.promise; + this.loading.set(identifier, loading); + this.fetchIconData(identifier).then( + (image) => { + if (!this.cache.has(identifier)) { + this.cache.set(identifier, image); + } + this.loading.delete(identifier); + deferred.resolve(image); + }, + (err) => { + this.loading.delete(identifier); + deferred.reject(err); + } + ); + } + return loading; + } + + private async fetchIconData( + identifier: NativeImageIdentifier + ): Promise { + const path = `nativeImage/${nativeImages[identifier]}`; + const endpoint = new Endpoint({ path }).getRestUrl().toString(); + const response = await fetch(endpoint); + const arrayBuffer = await response.arrayBuffer(); + const view = new Uint8Array(arrayBuffer); + const buffer = Buffer.alloc(arrayBuffer.byteLength); + buffer.forEach((_, index) => (buffer[index] = view[index])); + const image = nativeImage.createFromBuffer(buffer); + return this.maybeResize(image); + } + + private maybeResize(image: NativeImage): NativeImage { + const currentSize = image.getSize(); + if (sizeEquals(currentSize, preferredSize)) { + return image; + } + return image.resize(preferredSize); + } +} + +const pixel = 16; +const preferredSize: Size = { height: pixel, width: pixel }; +function sizeEquals(left: Size, right: Size): boolean { + return left.height === right.height && left.width === right.width; +} diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts index bcb313a6f..4a2f2faf4 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts @@ -1,7 +1,9 @@ import * as remote from '@theia/core/electron-shared/@electron/remote'; +import { NativeImage } from '@theia/core/electron-shared/electron'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { + ActionMenuNode, CommandMenuNode, CompoundMenuNode, CompoundMenuNodeRole, @@ -278,6 +280,12 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory { delete menuItem.click; } } + + // Native image customization for IDE2 + if (isMenuNodeWithNativeImage(node)) { + menuItem.icon = node.action.nativeImage; + } + parentItems.push(menuItem); if (this.commandRegistry.getToggledHandler(commandId, ...args)) { @@ -314,3 +322,23 @@ const AlwaysVisibleSubmenus: MenuPath[] = [ ArduinoMenus.TOOLS__PORTS_SUBMENU, // #655 ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, // #569 ]; + +// Theia does not support icons for electron menu items. +// This is a hack to show a cloud icon as a native image for the cloud sketches in `File` > `Open Recent` menu. +type MenuNodeWithNativeImage = MenuNode & { + action: ActionMenuNode & { nativeImage: NativeImage }; +}; +type ActionMenuNodeWithNativeImage = ActionMenuNode & { + nativeImage: NativeImage; +}; +function isMenuNodeWithNativeImage( + node: MenuNode +): node is MenuNodeWithNativeImage { + if (node instanceof ActionMenuNode) { + const action: unknown = node['action']; + if ((action).nativeImage !== undefined) { + return true; + } + } + return false; +} diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 5cd81f7c3..349faf482 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -119,6 +119,7 @@ import { PluginDeployer_GH_12064, } from './theia/plugin-ext/plugin-deployer'; import { SettingsReader } from './settings-reader'; +import { NativeImageDataProvider } from './native-image-data-provider'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -406,6 +407,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope(); bind(SettingsReader).toSelf().inSingletonScope(); + // to serve native images for the electron menus + bind(NativeImageDataProvider).toSelf().inSingletonScope(); + bind(BackendApplicationContribution).toService(NativeImageDataProvider); }); function bindChildLogger(bind: interfaces.Bind, name: string): void { diff --git a/arduino-ide-extension/src/node/native-image-data-provider.ts b/arduino-ide-extension/src/node/native-image-data-provider.ts new file mode 100644 index 000000000..beb7b265f --- /dev/null +++ b/arduino-ide-extension/src/node/native-image-data-provider.ts @@ -0,0 +1,61 @@ +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; +import { Application } from '@theia/core/shared/express'; +import { injectable } from '@theia/core/shared/inversify'; +import { promises as fs } from 'fs'; +import { join } from 'path'; +import { ErrnoException } from './utils/errors'; + +@injectable() +export class NativeImageDataProvider implements BackendApplicationContribution { + private readonly rootPath = join(__dirname, '../../src/node/static/icons'); + private readonly dataCache = new Map>(); + + onStart(): void { + console.log(`Serving native images from ${this.rootPath}`); + } + + configure(app: Application): void { + app.get('/nativeImage/:filename', async (req, resp) => { + const filename = req.params.filename; + if (!filename) { + resp.status(400).send('Bad Request'); + return; + } + try { + const data = await this.getOrCreateData(filename); + if (!data) { + resp.status(404).send('Not found'); + return; + } + resp.send(data); + } catch (err) { + resp.status(500).send(err instanceof Error ? err.message : String(err)); + } + }); + } + + private async getOrCreateData(filename: string): Promise { + let data = this.dataCache.get(filename); + if (!data) { + const deferred = new Deferred(); + data = deferred.promise; + this.dataCache.set(filename, data); + const path = join(this.rootPath, filename); + fs.readFile(path).then( + (buffer) => deferred.resolve(buffer), + (err) => { + if (ErrnoException.isENOENT(err)) { + console.error(`File not found: ${path}`); + deferred.resolve(undefined); + } else { + console.error(`Failed to load file: ${path}`, err); + this.dataCache.delete(filename); + deferred.reject(err); + } + } + ); + } + return data; + } +} diff --git a/arduino-ide-extension/src/node/static/icons/cloud.png b/arduino-ide-extension/src/node/static/icons/cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..fb4426ce8c17a9b9992ff382c89b3813f30d28d1 GIT binary patch literal 9457 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 Date: Mon, 27 Mar 2023 18:33:57 +0200 Subject: [PATCH 2/2] feat: new icons + dispatch based on the theme Signed-off-by: Akos Kitta --- .../contributions/open-recent-sketch.ts | 54 ++++++++++++----- .../src/browser/native-image-cache.ts | 55 +++++++++++++----- .../src/node/static/icons/cloud-dark.png | Bin 0 -> 319 bytes .../src/node/static/icons/cloud-dark@2x.png | Bin 0 -> 586 bytes .../src/node/static/icons/cloud-dark@3x.png | Bin 0 -> 834 bytes .../src/node/static/icons/cloud-light.png | Bin 0 -> 291 bytes .../src/node/static/icons/cloud-light@2x.png | Bin 0 -> 509 bytes .../src/node/static/icons/cloud-light@3x.png | Bin 0 -> 686 bytes .../src/node/static/icons/cloud.png | Bin 9457 -> 0 bytes 9 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 arduino-ide-extension/src/node/static/icons/cloud-dark.png create mode 100644 arduino-ide-extension/src/node/static/icons/cloud-dark@2x.png create mode 100644 arduino-ide-extension/src/node/static/icons/cloud-dark@3x.png create mode 100644 arduino-ide-extension/src/node/static/icons/cloud-light.png create mode 100644 arduino-ide-extension/src/node/static/icons/cloud-light@2x.png create mode 100644 arduino-ide-extension/src/node/static/icons/cloud-light@3x.png delete mode 100644 arduino-ide-extension/src/node/static/icons/cloud.png diff --git a/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts index c17fd79a4..dfc02b7a4 100644 --- a/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts @@ -1,4 +1,5 @@ import { NativeImage } from '@theia/core/electron-shared/electron'; +import { ThemeService } from '@theia/core/lib/browser/theming'; import { Disposable, DisposableCollection, @@ -9,7 +10,11 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { SketchesError } from '../../common/protocol'; import { ConfigServiceClient } from '../config/config-service-client'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { NativeImageCache } from '../native-image-cache'; +import { + isThemeNativeImage, + NativeImageCache, + ThemeNativeImage, +} from '../native-image-cache'; import { NotificationCenter } from '../notification-center'; import { CloudSketchContribution } from './cloud-contribution'; import { CommandRegistry, MenuModelRegistry, Sketch } from './contribution'; @@ -27,21 +32,34 @@ export class OpenRecentSketch extends CloudSketchContribution { private readonly imageCache: NativeImageCache; @inject(ConfigServiceClient) private readonly configServiceClient: ConfigServiceClient; + @inject(ThemeService) + private readonly themeService: ThemeService; - private readonly toDispose = new DisposableCollection(); - private cloudImage: NativeImage | undefined; + private readonly toDisposeBeforeRegister = new DisposableCollection(); + private readonly toDispose = new DisposableCollection( + this.toDisposeBeforeRegister + ); + private cloudImage: NativeImage | ThemeNativeImage; override onStart(): void { - this.notificationCenter.onRecentSketchesDidChange(({ sketches }) => - this.refreshMenu(sketches) - ); - this.imageCache - .getImage('cloud') - .then((image) => (this.cloudImage = image)); + this.toDispose.pushAll([ + this.notificationCenter.onRecentSketchesDidChange(({ sketches }) => + this.refreshMenu(sketches) + ), + this.themeService.onDidColorThemeChange(() => this.update()), + ]); + } + + onStop(): void { + this.toDispose.dispose(); } override async onReady(): Promise { this.update(); + this.imageCache.getImage('cloud').then((image) => { + this.cloudImage = image; + this.update(); + }); } override registerMenus(registry: MenuModelRegistry): void { @@ -65,7 +83,7 @@ export class OpenRecentSketch extends CloudSketchContribution { private register(sketches: Sketch[]): void { const order = 0; - this.toDispose.dispose(); + this.toDisposeBeforeRegister.dispose(); for (const sketch of sketches) { const { uri } = sketch; const command = { id: `arduino-open-recent--${uri}` }; @@ -95,7 +113,7 @@ export class OpenRecentSketch extends CloudSketchContribution { ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, menuAction ); - this.toDispose.pushAll([ + this.toDisposeBeforeRegister.pushAll([ new DisposableCollection( Disposable.create(() => this.commandRegistry.unregisterCommand(command) @@ -109,13 +127,23 @@ export class OpenRecentSketch extends CloudSketchContribution { } private assignImage(sketch: Sketch, menuAction: MenuAction): MenuAction { - if (this.cloudImage) { + const image = this.nativeImageForTheme(); + if (image) { const dataDirUri = this.configServiceClient.tryGetDataDirUri(); const isCloud = this.createFeatures.isCloud(sketch, dataDirUri); if (isCloud) { - Object.assign(menuAction, { nativeImage: this.cloudImage }); + Object.assign(menuAction, { nativeImage: image }); } } return menuAction; } + + private nativeImageForTheme(): NativeImage | undefined { + const image = this.cloudImage; + if (isThemeNativeImage(image)) { + const themeType = this.themeService.getCurrentTheme().type; + return themeType === 'light' ? image.light : image.dark; + } + return image; + } } diff --git a/arduino-ide-extension/src/browser/native-image-cache.ts b/arduino-ide-extension/src/browser/native-image-cache.ts index 268a96ae7..13948fee6 100644 --- a/arduino-ide-extension/src/browser/native-image-cache.ts +++ b/arduino-ide-extension/src/browser/native-image-cache.ts @@ -12,17 +12,32 @@ import fetch from 'cross-fetch'; const nativeImageIdentifierLiterals = ['cloud'] as const; export type NativeImageIdentifier = typeof nativeImageIdentifierLiterals[number]; -export const nativeImages: Record = { - cloud: 'cloud.png', +export const nativeImages: Record< + NativeImageIdentifier, + string | { light: string; dark: string } +> = { + cloud: { light: 'cloud-light.png', dark: 'cloud-dark.png' }, }; +export interface ThemeNativeImage { + readonly light: NativeImage; + readonly dark: NativeImage; +} + +export function isThemeNativeImage(arg: unknown): arg is ThemeNativeImage { + return ( + typeof arg === 'object' && + (arg).light !== undefined && + (arg).dark !== undefined + ); +} + +type Image = NativeImage | ThemeNativeImage; + @injectable() export class NativeImageCache implements FrontendApplicationContribution { - private readonly cache = new Map(); - private readonly loading = new Map< - NativeImageIdentifier, - Promise - >(); + private readonly cache = new Map(); + private readonly loading = new Map>(); onStart(): void { Object.keys(nativeImages).forEach((identifier: NativeImageIdentifier) => @@ -30,21 +45,21 @@ export class NativeImageCache implements FrontendApplicationContribution { ); } - tryGetImage(identifier: NativeImageIdentifier): NativeImage | undefined { + tryGetImage(identifier: NativeImageIdentifier): Image | undefined { return this.cache.get(identifier); } - async getImage(identifier: NativeImageIdentifier): Promise { + async getImage(identifier: NativeImageIdentifier): Promise { const image = this.cache.get(identifier); if (image) { return image; } let loading = this.loading.get(identifier); if (!loading) { - const deferred = new Deferred(); + const deferred = new Deferred(); loading = deferred.promise; this.loading.set(identifier, loading); - this.fetchIconData(identifier).then( + this.fetchImage(identifier).then( (image) => { if (!this.cache.has(identifier)) { this.cache.set(identifier, image); @@ -61,10 +76,20 @@ export class NativeImageCache implements FrontendApplicationContribution { return loading; } - private async fetchIconData( - identifier: NativeImageIdentifier - ): Promise { - const path = `nativeImage/${nativeImages[identifier]}`; + private async fetchImage(identifier: NativeImageIdentifier): Promise { + const value = nativeImages[identifier]; + if (typeof value === 'string') { + return this.fetchIconData(value); + } + const [light, dark] = await Promise.all([ + this.fetchIconData(value.light), + this.fetchIconData(value.dark), + ]); + return { light, dark }; + } + + private async fetchIconData(filename: string): Promise { + const path = `nativeImage/${filename}`; const endpoint = new Endpoint({ path }).getRestUrl().toString(); const response = await fetch(endpoint); const arrayBuffer = await response.arrayBuffer(); diff --git a/arduino-ide-extension/src/node/static/icons/cloud-dark.png b/arduino-ide-extension/src/node/static/icons/cloud-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..464646dd03e5c702e9fbea9b26f26b82bf197606 GIT binary patch literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG znBBdfl2dAR;}^)xU)=1$x)z%gTZ7?pXq~>tyedjTx}8F zvPLlP*wGUiM`sm1^eyv9H@g(1Rm}P!TrS(`_op>B+0|jrl_sa{=5GwKUm# KpUXO@geCyIR(5&- literal 0 HcmV?d00001 diff --git a/arduino-ide-extension/src/node/static/icons/cloud-dark@2x.png b/arduino-ide-extension/src/node/static/icons/cloud-dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..14af1ce5efa68f16c84535fd297d30f041b899e0 GIT binary patch literal 586 zcmV-Q0=4~#P)3 z!!QsA;5Uc?7{Q%@c7t>Rx&hsQZcuduHh>8b6SPb~H;6Z&8<;!A6+*(-#G#Gt-Xop( zk*cyU$N8gxQc5YMlu}A5rLIkdGjPt;^yh(MN@0Ak1I6CQdsS6Cl!PX}q-b1s{MjXP zN0TrVTNkgPm{P1L-rVmbe);d_`im~2? zrI=GAwHRPX0vh~u_pw-t871L>NH>V!+Rkk-K0~p>08tf?$lE{Sw?B!kg5DM2+D(o@ zI=BZd5$P8FEs&lkdNEu9yP-`nh}T}%uECdF^JA=Ck9|U?9^l#~Z;P}_Ut-a5i9DHr z3@3kN(IE+45|C2I@5|!EK0#as`2Pv{PM#T@*e3)lz?Xi5{E7PnsRC-0LEI-u33x;~ z>d*rz0S4u`4?U22fI~s32mCqLsMBCcqy$)$!-gJ63D~0?HuOMBzz53l8hRi#L0&(3 zBb3Gs>@btinIKpJe&_Uz{B5KIk$4#LRlqmD26?hH`HA!aaT)YgFh`y%f<-%a(bZpw zblM2<7x_}!AZ=W+JjPkqyTRVq;=UScw1*{wG=3c6Jqs_}FGGGLQA#PLlu}A5rPOu! Y29LF^%08_UXaE2J07*qoM6N<$f*H^Hy8r+H literal 0 HcmV?d00001 diff --git a/arduino-ide-extension/src/node/static/icons/cloud-dark@3x.png b/arduino-ide-extension/src/node/static/icons/cloud-dark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..4d857c1d5a14b41bca08a8c2bae5b9144a234787 GIT binary patch literal 834 zcmV-I1HJr-P)qlui&fC`^!a0>T9L2F(WI39DSMw8?vpl$@Ra zyVE&$Rhv{#dX{B5A^-pY00000000000072jB1aZNnCfAs<%5c zIW%i67dqYDNMR)d(8w?w|ItB$@J0%V!&rvh_))yZxRJuLkAReV8X1pE9R=T|kP-oi zdB2wNh$-l-R4U4vf>Q6VWnAKn=!+DRbp&Vt7J8V=_-u3pyp;lSpM*ckIK^d@S_;S( zfb;?Hl@w1Futz{X~l0~ko<-N}V`axpa8N6lepoAe12FU@=0k9;9hp;5>iv~q7uVX6c?8h=UCe7orV)@X!7G84dF{xW zf@*9yCPsm0FKNWfL7quS}Ius zuoif=mP*zD{1$lKTPj%tuo8H!ER{TKK@T$ST@N|6Is-kkRP@Y(ZUt7?){34H5PN^T z6X)TN#Cz7KHZ*c&?6!J;^9yX9@dng{!AXxK+&Asm? zgPDTz2FFtzJqK7iIJu-tn9G}|DKAgZb`)7zc=>|Kw%@jw%DbYT{!dmfV^V^EqO5Zg z?hlKe7d=k8Rgpd2b+-BRa--l~5z}|2=lq`iL~^?2U+(+YIP9b3V&i_^J`(!-zUqEu zO|9=H>XGuAQ%|c-{BZQT@DZVd83r=nPq|-bIbCuz-z;&`%K)R=kM=VS8NF?IKU^;O zpka|-&su2PSMyPoId+5n|26wm^}_Y$n`oakRsJ}4-HdPN?yHL`-;v-gJo`I#qfEgp zsY_b_9{6r>n46(vz3lM1*$J+j@AyvD|JU7kJI^^Pe7E+&Ql|{rfC=pntQ4;_uzw0H zk>xNwFrokTI_8DtO{SGMqh3$g>k#I|8!@Rr^?G9fv)Gg5o23d@8u&kbJjC$tr{wQ* z>+^lWCoU9x@crufCGR`y@0|amv0+K+R`X;_hQpot?ZG)ssY|B5;aQ=4Zk6rv!2O~U wv+iD*8o|0J?9FfciLx;TbZFuuKWJNLGOK%*&p7rBrmdCgC-o`ff%5U-=>tFO#11*2epB?$!Ps92zhe^%*|87&gnV$W9p8fv21g6G`Epqu(Z6ot>RuuQN z=P#3={am^Egqw@zzsojrPdTkNIlCoT$y#>-XZ2C7UyEi1e9B8dwMH~xX@sks_u2%r zuFwY?pUmzs2o78tadhi5H^()it2fNstk9D2{m(p=xjUCcuzpQjY@Fw`&*N$9dX1wt zs?}f1-O`H_<23)ZuM3tkoPFu^=|H2?w*u=9&rK44XKVa(6YJ6W8##Tf^V8QJ%JS5; zoH12q&+VXZkGMY=R{lP7{bAu1_nTK2{SfEq91ZU zbv&ju&SN{E9?2FdU7%YrbDM9JbO2}EeVyht;t#_tj%C!^ZeOU;z;@=$tp>LbSwLal zAIcS`(;Tm{{Sn>sP8Z0(!4(&MTH$^`65dFa)2t{C&r7w3n& zDA#BjrGL}PP+h6Y9=v*kwWn^SY56RM=%Bdse!`15^}Fp)%PL*I=yLg-;#R%#{ESPI zD@!GNPh39RdoS~p=EwJ0mnLL?y}u;1Yi;2Fg<9*|bB}J~fyXkC(IEJrG5*y28jeQk P+aPgIS3j3^P6zq`3TT32JQBD8=ctDJ~ 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