From 42f6237a62039132bf3d5457e37c5ad6f31e9031 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 8 Feb 2023 09:30:38 +0100 Subject: [PATCH] feat: icon for cloud sketch in File > Open Recent Ref: #1826 Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 5 + .../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 | 5 + .../src/node/native-image-data-provider.ts | 61 ++++++++++++ .../src/node/static/icons/cloud.png | Bin 0 -> 9457 bytes 7 files changed, 238 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 4743f3f84..284d5be8d 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -347,6 +347,7 @@ import { ConfigServiceClient } from './config/config-service-client'; import { ValidateSketch } from './contributions/validate-sketch'; import { RenameCloudSketch } from './contributions/rename-cloud-sketch'; import { CreateFeatures } from './create/create-features'; +import { NativeImageCache } from './native-image-cache'; export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -1014,4 +1015,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { }, })) .inSingletonScope(); + + // 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 812761f77..781633694 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -118,6 +118,7 @@ import { LocalDirectoryPluginDeployerResolverWithFallback, PluginDeployer_GH_12064, } from './theia/plugin-ext/plugin-deployer'; +import { NativeImageDataProvider } from './native-image-data-provider'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -403,6 +404,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .toSelf() .inSingletonScope(); rebind(PluginDeployer).to(PluginDeployer_GH_12064).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 zcmeHt`9D-&{QsR9OZJdt7lZ5~*|LSqB}?|*L@N6fMYfqqvSk}vg%qQ#p)4cYtwbrx zq{uo=Duj?_vW)NT{rUa{pPxVTn0eeg=brOA=XLJu^?W_g>zq`3TT32JQBD8=ctDJ~ z0{|f5TO_cL4Zf^J4{yO2mWY#}(?0kUx9?mE{GS7dagP81-kyIyM7}bw7+fe4Y2g;> z7=Au7>g?qJASxF<_wUs}4 z`{X_ZKW3kFB8Q`i$vFX*JkzEKPv~G%=h&DoRk*$`zGB>^vAyKcDOOK}dcX9Mv=o^` z{GzNP*O7kvG|~I_nF*YHSk3fzK6}Qcdaz-W`C0GhuHpO5Lu~H9JexOtYBqdO7!n%% zzwiHQ2CB5y&0S5Do_~h8vHA^i8WqM5k)CUN*97Larfc{1w7T|wBi4|1e1HEqLRyd} zRgDq>gGIQxus3Au9}yMkzt*ozEj zTTiV0eaS@2fz@U_x49mNK1cbc z*)R^4)rdWIH;cFn{xc%VgeYln{*dQas!=(UA=#iuBDZYB&^YJ@Lgk(G1R8(_LocY{ zj;~0h4s|uM>87v!#gK;C9wJ8gr-_G3_>Fr8648pW+W=SU?_;+H(_+ExQy7xwnoIHb z@H}Db73^WK-->U}Gc=O@Y-B>MT&Or2>oS?V2iMD zMWcQZlEJql2RHi4Apxr9>ptRl$Z>a%8AR>X(WbJru0Uw4OrU-9(W>}X))th{gw%NM zhBNN`2`|p#lUoaD#Ry(NzpsU?HRUVdodaYcT9HxQY3<}ceXy&v#vuPLyol)XiQP^5At(qQ23x)!r!8#pZ#TddJ2!z_8(vRQ!_men2J6MrJ z8YU@{U8{!pR?hoN1SX&4%MP46P3?JorDyL%QARMPNChO#Hy|{!7|BsjR}8UNzt&MZ z&!b!g_98-$cO?~NgVf3xOg+9_YHXmV4toAdCv|I)Z>N}18lkn)C@^M5((hMD!Mo{* zrW3ngy@_|$OCA0(WW0Im+wcC1+~7P*xE%BFBxVh{2kM`;{B)N)u4GLsrHgkyLmPS^ z*OY{AZr&ySOu=JD#5NK2Diw0WDT##kly1rSj6R}BMbiy3zaGDW-A4?2b<@jU-A?Ec zVb4;u;m8yVwbx1ty{M4g?wk7~t?A{uqil&~BTu0FlFRO1`<1=vB1{a*1$A83&|hJ5 z5AVAAQ@jvmz5NZO9{t3Z!{%*Q!yFWZ_k>5OTU~##T{t4RwZK1xgJQD>Rs=7#n1?1_xxaF z%{G0;L2NkKlNOGNP-L3K#n!4bO~&}>;h;gH)jUwI!2GJ|QOh>v=z^*|U$oL-=ePh^7gtEc3})-3kk=7}_t)g}2AzVsZrW?b_qcpRBvvU8=mYgX>cXx*47 z16afpT=*OJ>53J4S-F2n@8}+2vMPwsd{x+5LNr@tG2M&xZ=|y({8Z-z zpC7hqsUu&%g$h$(KG5)5Lp~}je5Sd=H`5+VixPWX&aRueR;Bc8YEeK36%m-#DMQZu zbCxcJeEG_QrYiTNuA?JAF6)rd*}P{X7>T`lt;*o`UL-A4cYn6hQ?6@a>+N@sS$j-= zU!3_XPqQQ6*?y^)xiPk(yGcRmUNQ_-t&-NpO0MYad~oCGXzA(5Z*9%_DUF@2h9g|* zIs1Csj9gdce^_1&mYVM35?R~gJhIRS@`nv zA+BNZ)`rKt1c9r<`JYzJ39P3>08i1U+dP$u1N5&PTO zwX^A<<;K@(yr-B&YCd~+oy7Q#`6jZ(xYijhXPl>K;4^ zNoZSH=FJ7!&|lZ0+p4P5JCYc|W3LU2lG@p;)7P_YN7B0Zw{cIUgnnDm>w{%~>q;F; zA5O}YXRreu@od+GC4Lb`gg$#X$ZyCp ze{A{8xm)1_&1n4e`aPL%Anh0Ag?&rETYc6um-m2yaC9-_s-lbw?F(XFV<@?zR<`qG zo=is~BeUX@8;93P{a;3X6~W(#BgjI;d;V#kx}KMi%d6vA2mDXW>D+uRHeW`4n)f+e ze9j;`vX6TD9s873?12BMc(ekJYh`gH#LI5yMqpJypFpf^9qUy^L+ikE3WC#B#{{}r zGRt=r%%ynV$#xkQrM}PmAn8yssIVcwW&2mrs$7&y1>NN+qWYMHSLqSQNeSf64aP?3 zvEl=jEzO9)IlCF2CSO>w^}p?-!31(p+A|4iY zW&8#MvGO5WfB^(Pwr#CMgqOv;@OotxeXQc~Hd)AsI0;wc`HNhUu#yV&y%i38?6=Vg z{zwSV8I#%idDNpA23Jf)CBizT{p`~3`oz5463kl%S%cg@P;e)&gS4|56kQOxhzb1~ z&@-hWrDkkVrRcgp%GWD-IxksEh?2(0OdwMb_^LBSy6F_L03M^6y!!G=<~=vAIF@d? zEHYbMN{8L0op$PzLe916o=LQ;Q+DZat>~Pfv-9T5Ta;36MKPdOF?~rBUN>`T>#fXZZDyWUek&orOK1zxH z=|Ale^lG4I@=%{^cB#{iZ%2MgQb=o4OkZ5E+F|eG;xtg1Hti;ddU0iY`{eHi8Mu!XgyS`SS|8+&rB=^U+c6Le=S%Hhs zy7%ias;Im(&2~`g_lL@#)Po}9qJ-Xna?yqZzMIoMgT^lolP87B4fc$vB6#^90;lqoc$kRKqwUTTf!y~=DG6^OpgKtLYFs0ysF^bb1zz-$%V`t*qARx{JR z<8#Cxt9hx8d=V*igEN8XGd~0OD${0Qt9(tqYIho#>k}TqmSIell7(#xJ*V@v}3fmPCaav2w$5bi6%PFRS zs?j39R95P2n&2RG^;^=&hNWLkIoqwn2{p8Q=wX0@5|2caV_{bIZCr`{`Q-J# zZn(eooNC>5dcZ}mVKKrhxD9n{=MY7;A&wnT#Q`5El?dqDf2C_Vw!ybFpBfLdumc(C zTP(}w_H8gLPA7aKewRAvcGWZDUv2kCU5Mlqxw)Q3!$Qjs;QpmEMJp$SoF1by zes~fC3;_Fp&rvDhm!r*TGcZT}28JVL?qzf`;zH8G0Z|>b$_ed%9bR-nxw7R?q80?5 z{zo>4>G#K+O0J~&OQF~=Pf>5fJb3|8VQqHl%LR3?=tsnWScf1%SUUJRcv( zN5eTJ8?%q5o)`-h4P?EJ{vM>ZJUMH{{e*CAKZ6ygjk626l2-&mmmocOjbJ!+nLJuo z%a%>P9%8ri^fKAaeC#P!O{0x`-2R6TOEF@n_*k3TR#~mW#FB%<63m$=%S#K_dpxL@ zdVjOEsFWYg%Swenva0fk0DeVVR6%$eO>&}K>oHar%g`prb5EJfD-R=L2#JLHA)|8Y z`m1ElFfmZ+<-Z!9sgJfJqAiAa%7cb6)9Y~T_ES=l$(qO`$LtEOynkCpccIx({2r^G zmWhAwj@e0ajP!DKQwANbO<{KaJehR^I?^d0vD(*F33&M>jWi|t^A{f0r2aov^wPqB zSAtMCvPk%|N6=U_&9G^2b2p|;c=Z15^SNZ|0tmNXaN~>vO5Yrg!91l43*ZksiL@X5 zaFyirFfju$rZC9Lu3=yy?r%&cJV+rL+P>y{-LU*ORs zhda7r{J~3RwIdX)c3pd_nan^0j$hM0tJZLXA?0VgVq&eT1b#|MaQPu%O>Au*E$|Ky zz#U#OrilAXEIFzw;)mY%PtZNdZTzCoZ6S%Cy@tz~lSN_PVD+iB2+cl*#2wY|k1brW z*~HH63-A&(!ob26Ucxtc6N?BVmagYacu|C-kUR7pI^XCV$i)kws|-U=-W_G?VZCtP zFG4xJ%yw$-TurM#{)tL*Dw+Lx&Y(w|j4-|ZS_kmVv85QuH1ICM_oQKFCJ+X+q&~)c zIuC3L7$bxGqAOE33dl+p#tP$#2R~SJj}9Ou<-3N)wPRLJ2Z7hp6Kq>NRoFJdsw+J0 z*A)n6#A_4-2W5EWI+UM{7CJAAGhD7#;;Oq0EQ`|m$f@`sn~@GR5?QU0e>vX4*FCkG z4X{KO0)k3P^&ju4KtvO!Ag_eT|CJ4^$~j z=HUTq9{PKrKb7Sf91y=Iu*4*5i#vt0!6a#7(zONluz% z^j@ZhZ;u~@eUIga zoxrq|wm??S5XW_Yj269ItYXTIV}~p737o=d4@JgtM`tpaRa2CL`S+_LpN;va53ToV zLu%iT!AYQu#eH62oUwEG&h+KG4-&SKHL2k#Lv&M`ja)OF9g7fWSCQ_T7FGRhziLaA zy|Zt^z>2lJT$mAw_l8qZedg4eWgq%>V-7v+`rwzpoAOGY0V;@0Rn=I~;gLb+jsgYW z(X=q)uTK4yJliuOJd!h{0)h_XF-Nc2Mf#!^96dJ7;ANY--h=by!n#~Kt2t<)3*3Qk zu#Zbw!XA`q?Vl3M_A+eb)NSkOkOL|J$ukXJ375(+MaQ-Ml1g*8d=2A;HTG6z2R~dTp;@aDIs(&KQ&SgTMYwIXvia^>lg36u zR^3ELKaijZt;%KmF^97toO5%hZ-0!KuUm+ww`%VQHk|1cDnKhbilO)f#viH4tuL1A zAE%VXLlZGdN}z+7ZYm?8`LTE8d%njr-nuJxq+z`vsHA73?q`O}8xH8>^ZdA%v!RNh zgQs57XxEkD_kbmv>#`5_bo{@<@k6Kyn-HnkSsEu#{3pqH>Xaq%%JVDP=&Fmq2u~A# zY4}Oug)hnGvO=R}Px8=;TK>fC)L$2T8x%j#bbxVWyHHWE*`QpB4;pk(@ZgG*ZP;;# zj=$VZ{Fow^UyEu{K5hq+o;)?IdY?WYZYNUt8g9j&mwmDGv0Nw<&d4gn0t^f6Uv}P| z2gfI-xt9l^C$JGXoGaa2eSr7ZqWgdYCi_QIJM@cyPTA8?-4*-rv@#r_KkZSj}?gP7BX z3gZuey=pHPhPt;;$&*ArVomsK8enV;96~LKI;DzT@6Sgoj&9EFMrg3<{**OlOm zIBB>pLBweZjja*YkgY@lI_w3ipaXfDrNFS0?KGd#jtDgq-rcxTZcN&Gu}>?A9G@yX z0gmOFkjP=fmikFLFV={906$D&TuEdv^Yx_hVS?&)PT&17kpkP?4mmC5i8frb|N4)~ zp%sPCbaUIK#-WxmFe^iU_d8?X<`_iI1!O6x`{=51LrJHl8PQ8cfQ1}92(_}U7pdCV zllEUF5h~#P)id#58KN3SmCK)C;zi0!cQ*ZQJB#>C;$^8GbE95@;8E5bB_~jVeg&29 z8IaIbABR(7g{0Q!O#h8+;HBqxSY5L20~Dc>W1kpnKFlxvb@54?)^`6z`BiDy5QO1A z`bC$|*?)EXpYeLK<+nGTnkeY-{Ga*{S@Xs8f!qB5X*YVfa-vTHz>p3oRI7V=pd@*f z;(p%0EC=>mIQcSgJkipX%U?|F{(DC?2z4H%ZKi00M&)wGiWUJ~6+3_qlOiPLpa4QV z>fgJi7DG>jKNBno`KChA*ZxhAg~5@^2_c9sI)|B>W_5mnfSY-C!mRnP$u!LCqS&1M z?C4ibNod7>mmV9SLy*ed^K(zeghyAiG6W=*iQ7zAQG~gHrG3>bP|bgxyf_LQkkpQF z5fDu5BDmeOL|K!b^_?Pjsh7R?wx0h}ibpMTUlK&-FL0VbaL2Og;J zt6^4x<3B-FRypxgJShC@005`E#CZgqkNZeWM7_m#)k~cKD97*1pkfG)$hU>)D$Yj& zI@{fRl!W*_oYB!-fh_{;cSu3l92CA0{`p;Q)}}g)PgtizpU%>-vYmvU7qa zc{QiDk}U;vH0MxE;FjW%Ga%_QoE1tQHQM-)qoj+7R-#;2`1C1{gl^8#5g2b(lZ(H8 zXhR z=nmsl3&-;Q#`t-Go9+QR6Ez#!P(a=JiLyFk7Rv~1MlQR`4K_FlHMtUmec^5@-n;SO zXw6)t9GKS3cZq$GCsQxOWe0}Gcr7)qJaluZC{A!?cXn~WiRmrk)WH`WrIlZZR{XFH z!|OKTV^P)4IqWsIe0=7bs0E39wL!h=ciNJ1A*x8bs7F`}bDz&`U&vI501VgXH0MH! z{)1~T<f~V<0*x?n#(MjO zg>vk9t&!}0+JTQw!t;0eM4(j3Y{C=1*^1N({I5SRxdlX1E)Wybtxm(adI==FL4y}h zQBFG@7EVo((XS##i|TNY(65SY^aItO`HH9bVBeSJcBmxwu=V*v)@im5B(nYh%R&BW zDc;QA1v#@zGk@a!r4IrIc{#OcQ1&**gk7AmuI^``y&xZ_wvFV@d__3}gRy=DCf+V` zPv@AC=Xq)_u}=$g`JxJYCY(?JNkFFbfLZ#|=FMI~C{N%I8@21jP%5u*Frl^Jb}LLn zI#Zj(4WD(3sZII#O}!W6nh=HK`jEAL?6r`nnlBKJ9jzl1}78aEKTFd zvWG6@6?DQr6v72(i+nwOYY$`HcN91)qwlO7&}eO1kONDWSg5V*v&;}O<6_{KEUXO9 zPB6btGX{Kg*hZy_2Mv~7FkAInKYlEM@AQ!>o>SV%9mcHOJ&EKuVkC6NjP9!Ew6Rw$ zn~w{eM_)AS(J}8_(XyIAo05iGt|Al6|1qOtcU!Gv582>Nh~7$_dnhBe5QkI#7}8k( zL$Y3IP0H`md3a+ZKK!5$x60wJGoq;qpf%g0f)A1;az`jfT=B89C!QGxp(R;;oKD6z z&EQrIl<(h*=GA2%eaw*0K?lIBZf04iXVW;NSCsbQg(q41!v1NKfnFurNov81Ff^=! zzshnDZSSL`;K+#ZZ^dV4(>8p2bR23{Smx>=Qxm%fb+TY}{}S|NChA4YfD#kx>Mg8h2}x(#~1ASLnV+DF$qGE zM{MlY`ks=`e&=6z19u2O?9MruhV1TbYukb*OAPD|i zlmn?-b!<#d%HxIW)niu6kH|M%oSp8$z;Rw^S)W#|#90%Qv3(X2f_jAt;fOKl0@SlV zeq0Tu*o1A!WrRIs$%8{x*gd3=nTMY1ol5Ve^SP(=;Ao)^I`AGuSmqSux+=}iH7Ah;YFTXA2h$(#o6!b~Vn82%u@oM_dX5oSF{<`3&%WdP>T+ODJ;54>)D@wm4l6X5T-{;O zD9#{8A*G}T1pkq_w*+-mDcWWuIJiU;IMg7rtVYE`FVu zz7YipT7*K@5ew4ry)aZ4GL9=+jiQ$HYZw7x*-_z#7oRVfnvuvkwbCVEgj(p6;(GT< z4Or&Uz}T-J38=vLLiTRPd&88G`0ZtDmZX}@F5Wb5s9u_QS)`p`}} zB;z@jaUoZ#N9s_7=MP~MXbC-0wpR~h%Y~zXS