From ea834d826a661a18bdc71311fa8f78225ec3130b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 21 Oct 2018 16:19:36 -0600 Subject: [PATCH] Add TradingView widget Fixes https://github.com/turt2live/matrix-dimension/issues/132 --- .../20181021144645-AddGrafanaWidget.ts | 1 - .../20181021152145-AddTradingViewWidget.ts | 23 ++++ web/app/app.module.ts | 4 + web/app/app.routing.ts | 8 ++ .../google-calendar/gcal.widget.component.ts | 2 +- .../tradingview.widget.component.html | 21 ++++ .../tradingview.widget.component.scss | 0 .../tradingview.widget.component.ts | 98 ++++++++++++++++++ web/app/home/home.component.html | 8 +- web/app/shared/models/widget.ts | 1 + .../shared/registry/integrations.registry.ts | 6 +- .../tradingview/tradingview.component.html | 2 + .../tradingview/tradingview.component.scss | 8 ++ .../tradingview/tradingview.component.ts | 42 ++++++++ web/public/img/avatars/tradingview.png | Bin 0 -> 4689 bytes 15 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 src/db/migrations/20181021152145-AddTradingViewWidget.ts create mode 100644 web/app/configs/widget/tradingview/tradingview.widget.component.html create mode 100644 web/app/configs/widget/tradingview/tradingview.widget.component.scss create mode 100644 web/app/configs/widget/tradingview/tradingview.widget.component.ts create mode 100644 web/app/widget-wrappers/tradingview/tradingview.component.html create mode 100644 web/app/widget-wrappers/tradingview/tradingview.component.scss create mode 100644 web/app/widget-wrappers/tradingview/tradingview.component.ts create mode 100644 web/public/img/avatars/tradingview.png diff --git a/src/db/migrations/20181021144645-AddGrafanaWidget.ts b/src/db/migrations/20181021144645-AddGrafanaWidget.ts index a2caa60..6ebfc73 100644 --- a/src/db/migrations/20181021144645-AddGrafanaWidget.ts +++ b/src/db/migrations/20181021144645-AddGrafanaWidget.ts @@ -1,5 +1,4 @@ import { QueryInterface } from "sequelize"; -import { DataType } from "sequelize-typescript"; export default { up: (queryInterface: QueryInterface) => { diff --git a/src/db/migrations/20181021152145-AddTradingViewWidget.ts b/src/db/migrations/20181021152145-AddTradingViewWidget.ts new file mode 100644 index 0000000..328893b --- /dev/null +++ b/src/db/migrations/20181021152145-AddTradingViewWidget.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkInsert("dimension_widgets", [ + { + type: "tradingview", + name: "TradingView", + avatarUrl: "/img/avatars/tradingview.png", + isEnabled: true, + isPublic: true, + description: "Monitor your favourite cryptocurrencies", + } + ])); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkDelete("dimension_widgets", { + type: "tradingview", + })); + } +} \ No newline at end of file diff --git a/web/app/app.module.ts b/web/app/app.module.ts index cc683d4..c341a25 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -97,6 +97,8 @@ import { GitterBridgeConfigComponent } from "./configs/bridge/gitter/gitter.brid import { GitterApiService } from "./shared/services/integrations/gitter-api.service"; import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component"; +import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component"; +import { TradingViewWidgetWrapperComponent } from "./widget-wrappers/tradingview/tradingview.component"; @NgModule({ imports: [ @@ -177,6 +179,8 @@ import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.w GitterBridgeConfigComponent, GenericFullscreenWidgetWrapperComponent, GrafanaWidgetConfigComponent, + TradingViewWidgetConfigComponent, + TradingViewWidgetWrapperComponent, // Vendor ], diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 1215f2a..e0c926b 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -35,6 +35,8 @@ import { AdminGitterBridgeComponent } from "./admin/bridges/gitter/gitter.compon import { GitterBridgeConfigComponent } from "./configs/bridge/gitter/gitter.bridge.component"; import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component"; +import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component"; +import { TradingViewWidgetWrapperComponent } from "./widget-wrappers/tradingview/tradingview.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -167,6 +169,11 @@ const routes: Routes = [ component: GrafanaWidgetConfigComponent, data: {breadcrumb: "Grafana Widgets", name: "Grafana Widgets"}, }, + { + path: "tradingview", + component: TradingViewWidgetConfigComponent, + data: {breadcrumb: "TradingView Widgets", name: "TradingView Widgets"}, + }, ], }, { @@ -225,6 +232,7 @@ const routes: Routes = [ {path: "gcal", component: GCalWidgetWrapperComponent}, {path: "stickerpicker", component: StickerPickerWidgetWrapperComponent}, {path: "generic-fullscreen", component: GenericFullscreenWidgetWrapperComponent}, + {path: "tradingview", component: TradingViewWidgetWrapperComponent}, ] }, ]; diff --git a/web/app/configs/widget/google-calendar/gcal.widget.component.ts b/web/app/configs/widget/google-calendar/gcal.widget.component.ts index 3b1e370..7d88dda 100644 --- a/web/app/configs/widget/google-calendar/gcal.widget.component.ts +++ b/web/app/configs/widget/google-calendar/gcal.widget.component.ts @@ -38,6 +38,6 @@ export class GoogleCalendarWidgetConfigComponent extends WidgetComponent { } const encodedId = encodeURIComponent(widget.dimension.newData.src); - widget.dimension.newUrl = window.location.origin + "/widget/gcal?calendarId=" + encodedId; + widget.dimension.newUrl = window.location.origin + "/widgets/gcal?calendarId=" + encodedId; } } \ No newline at end of file diff --git a/web/app/configs/widget/tradingview/tradingview.widget.component.html b/web/app/configs/widget/tradingview/tradingview.widget.component.html new file mode 100644 index 0000000..c72e682 --- /dev/null +++ b/web/app/configs/widget/tradingview/tradingview.widget.component.html @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/web/app/configs/widget/tradingview/tradingview.widget.component.scss b/web/app/configs/widget/tradingview/tradingview.widget.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/configs/widget/tradingview/tradingview.widget.component.ts b/web/app/configs/widget/tradingview/tradingview.widget.component.ts new file mode 100644 index 0000000..397d57d --- /dev/null +++ b/web/app/configs/widget/tradingview/tradingview.widget.component.ts @@ -0,0 +1,98 @@ +import { DISABLE_AUTOMATIC_WRAPPING, WidgetComponent } from "../widget.component"; +import { EditableWidget, WIDGET_TRADINGVIEW } from "../../../shared/models/widget"; +import { Component } from "@angular/core"; + +@Component({ + templateUrl: "tradingview.widget.component.html", + styleUrls: ["tradingview.widget.component.scss"], +}) +export class TradingViewWidgetConfigComponent extends WidgetComponent { + + public readonly intervals = [ + {value: '1', label: '1 Minute'}, + {value: '3', label: '3 Minutes'}, + {value: '5', label: '5 Minutes'}, + {value: '15', label: '15 Minutes'}, + {value: '30', label: '30 Minutes'}, + {value: '60', label: '1 Hour'}, + {value: '120', label: '2 Hours'}, + {value: '180', label: '3 Hours'}, + {value: '240', label: '4 Hours'}, + {value: 'D', label: '1 Day'}, + {value: 'W', label: '1 Week'}, + ]; + + public readonly pairs = [ + // USD + {value: 'COINBASE:BTCUSD', label: 'Bitcoin / US Dollar'}, + {value: 'COINBASE:ETHUSD', label: 'Ethereum / US Dollar'}, + {value: 'COINBASE:LTCUSD', label: 'Litecoin / US Dollar'}, + {value: 'BITTREX:SNTUSD', label: 'Status Network Token / US Dollar'}, + {value: 'BITTREX:ETCUSD', label: 'Ethereum Classic / US Dollar'}, + {value: 'BITFINEX:BTGUSD', label: 'BTG / US Dollar'}, + {value: 'BITTREX:DASHUSD', label: 'Dash / US Dollar'}, + {value: 'BITFINEX:EOSUSD', label: 'EOS / US Dollar'}, + {value: 'BITFINEX:IOTUSD', label: 'IOTA / US Dollar'}, + {value: 'BITTREX:LSKUSD', label: 'Lisk / US Dollar'}, + {value: 'BITTREX:OMGUSD', label: 'OmiseGo / US Dollar'}, + {value: 'BITTREX:NEOUSD', label: 'NEO / US Dollar'}, + {value: 'BITTREX:XRPUSD', label: 'Ripple / US Dollar'}, + {value: 'BITFINEX:ZECUSD', label: 'Zcash / US Dollar'}, + {value: 'BITFINEX:XMRUSD', label: 'Monero / US Dollar'}, + + // Euro / GBP + {value: 'COINBASE:BTCEUR', label: 'Bitcoin / Euro'}, + {value: 'COINBASE:ETHEUR', label: 'Ethereum / Euro'}, + {value: 'COINBASE:LTCEUR', label: 'Litecoin / Euro'}, + {value: 'COINBASE:BTCGBP', label: 'Bitcoin / GBP'}, + + // Bitcoin + {value: 'COINBASE:ETHBTC', label: 'Ethereum / Bitcoin'}, + {value: 'COINBASE:LTCBTC', label: 'Litecoin / Bitcoin'}, + {value: 'BITTREX:SNTBTC', label: 'Status Network Token / Bitcoin'}, + {value: 'BITTREX:BCCBTC', label: 'Bitcoin Cash / Bitcoin'}, + {value: 'BITTREX:ADABTC', label: 'Ada / Bitcoin'}, + {value: 'BITTREX:ARKBTC', label: 'Ark / Bitcoin'}, + {value: 'BITTREX:EMC2BTC', label: 'Einsteinium / Bitcoin'}, + {value: 'BITFINEX:IOTBTC', label: 'IOTA / Bitcoin'}, + {value: 'BITTREX:LSKBTC', label: 'Lisk / Bitcoin'}, + {value: 'BITTREX:NEOBTC', label: 'Neo / Bitcoin'}, + {value: 'BITTREX:OMGBTC', label: 'OmiseGO / Bitcoin'}, + {value: 'BITTREX:POWRBTC', label: 'PowerLedger / Bitcoin'}, + {value: 'BITTREX:STRATBTC', label: 'Stratis / Bitcoin'}, + {value: 'BITTREX:TRIGBTC', label: 'TRIG Token / Bitcoin'}, + {value: 'BITTREX:VTCBTC', label: 'Vertcoin / Bitcoin'}, + {value: 'BITTREX:XLMBTC', label: 'Lumen / Bitcoin'}, + {value: 'BITTREX:XRPBTC', label: 'Ripple / Bitcoin'}, + + // Misc + {value: 'BITTREX:BTCUSDT', label: 'Bitcoin / Tether USD'}, + {value: 'BITTREX:ETHUSDT', label: 'Ethereum / Tether USD'}, + {value: 'BITTREX:SNTETH', label: 'Status Network Token / Ethereum'}, + {value: 'BITTREX:BCCUSDT', label: 'Bitcoin Cash / Tether USD'}, + {value: 'BITTREX:NEOUSDT', label: 'Neo / Tether'}, + ]; + + constructor() { + super(WIDGET_TRADINGVIEW, "TradingView Chart", DISABLE_AUTOMATIC_WRAPPING, "tradingView"); + } + + protected OnNewWidgetPrepared(widget: EditableWidget): void { + widget.dimension.newData.interval = "D"; // 1 day + widget.dimension.newData.pair = this.pairs[0].value; + } + + protected OnWidgetBeforeAdd(widget: EditableWidget): void { + this.setViewUrl(widget); + } + + protected OnWidgetBeforeEdit(widget: EditableWidget) { + this.setViewUrl(widget); + } + + private setViewUrl(widget: EditableWidget) { + const pair = this.pairs.find(p => p.value === widget.dimension.newData.pair); + widget.dimension.newTitle = pair ? pair.label : null; + widget.dimension.newUrl = window.location.origin + "/widgets/tradingview?pair=$pair&interval=$interval"; + } +} \ No newline at end of file diff --git a/web/app/home/home.component.html b/web/app/home/home.component.html index dfd1185..9f42ff9 100644 --- a/web/app/home/home.component.html +++ b/web/app/home/home.component.html @@ -40,8 +40,8 @@ Etherpad
- - Grafana + + TradingView
@@ -51,6 +51,10 @@ Twitch Livestream
+
+ + Grafana +
Google Docs diff --git a/web/app/shared/models/widget.ts b/web/app/shared/models/widget.ts index 65f7ec8..828834f 100644 --- a/web/app/shared/models/widget.ts +++ b/web/app/shared/models/widget.ts @@ -9,6 +9,7 @@ export const WIDGET_YOUTUBE = ["youtube", "dimension-youtube"]; export const WIDGET_GRAFANA = ["grafana", "dimension-grafana"]; export const WIDGET_TWITCH = ["twitch", "dimension-twitch"]; export const WIDGET_STICKER_PICKER = ["m.stickerpicker"]; +export const WIDGET_TRADINGVIEW = ["tradingview", "dimension-tradingview"]; export interface EditableWidget { /** diff --git a/web/app/shared/registry/integrations.registry.ts b/web/app/shared/registry/integrations.registry.ts index 09de201..27b12ba 100644 --- a/web/app/shared/registry/integrations.registry.ts +++ b/web/app/shared/registry/integrations.registry.ts @@ -6,6 +6,7 @@ import { WIDGET_GOOGLE_DOCS, WIDGET_GRAFANA, WIDGET_JITSI, + WIDGET_TRADINGVIEW, WIDGET_TWITCH, WIDGET_YOUTUBE } from "../models/widget"; @@ -51,7 +52,10 @@ export class IntegrationsRegistry { }, "grafana": { types: WIDGET_GRAFANA, - } + }, + "tradingview": { + types: WIDGET_TRADINGVIEW, + }, }, }; diff --git a/web/app/widget-wrappers/tradingview/tradingview.component.html b/web/app/widget-wrappers/tradingview/tradingview.component.html new file mode 100644 index 0000000..4a8002a --- /dev/null +++ b/web/app/widget-wrappers/tradingview/tradingview.component.html @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/web/app/widget-wrappers/tradingview/tradingview.component.scss b/web/app/widget-wrappers/tradingview/tradingview.component.scss new file mode 100644 index 0000000..4e9c7e5 --- /dev/null +++ b/web/app/widget-wrappers/tradingview/tradingview.component.scss @@ -0,0 +1,8 @@ +// component styles are encapsulated and only applied to their components +#tradingviewContainer { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; +} \ No newline at end of file diff --git a/web/app/widget-wrappers/tradingview/tradingview.component.ts b/web/app/widget-wrappers/tradingview/tradingview.component.ts new file mode 100644 index 0000000..c1335fd --- /dev/null +++ b/web/app/widget-wrappers/tradingview/tradingview.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import * as $ from "jquery"; + +declare var TradingView: any; + +@Component({ + selector: "my-tradingview-widget-wrapper", + templateUrl: "tradingview.component.html", + styleUrls: ["tradingview.component.scss"], +}) +export class TradingViewWidgetWrapperComponent implements OnInit { + + private symbol: string; + private interval: string; + + constructor(activatedRoute: ActivatedRoute) { + let params: any = activatedRoute.snapshot.queryParams; + + this.symbol = params.pair; + this.interval = params.interval; + } + + public ngOnInit() { + $.getScript("https://s3.tradingview.com/tv.js", () => { + new TradingView.widget({ + "autosize": true, + "symbol": this.symbol, + "interval": this.interval, + "timezone": "Etc/UTC", + "theme": "Light", + "style": "1", + "locale": "en", + "toolbar_bg": "#f1f3f6", + "enable_publishing": false, + "hide_top_toolbar": true, + "hide_legend": true, + "container_id": "tradingviewContainer", + }); + }); + } +} diff --git a/web/public/img/avatars/tradingview.png b/web/public/img/avatars/tradingview.png new file mode 100644 index 0000000000000000000000000000000000000000..517de82cd08a04ffe38c542c202b244e75ed3cad GIT binary patch literal 4689 zcma)AcQD*v*H=RpA=V-YQ4&@OiMDzK!77PPtmrLz3!-h*1lg#ouf#+A*tOc~qL(Z| zEYZuVD-o;`A|am5JMTR6%=_2-ow?t0=G-%P?w#-Xe$MB9UOh6_WdZO2sHmt|^!2n& zDK_lyV7x?mE4e5TDCUxrfvz^iQl3+-wit@J?5}4NL`B5}{5zkz z1LW5V&$H^PrKNw5(+5id8D)dvG_;;cWHEg`0+NkiN^pmbF%9Ag)QScAf45HO=#&~p z-t=nNTl@_vk6BpkL(R-(<0Ps+osRyz1e>&pMf`%mz{r~fO+*^Ei>&jhgD8?azDR6cq*$7iU-Oo9!h@L#hv&2qTR zn8(JHFUe9M=~uVqbGgRRBuzdb1un=I;{?|cF7{3Xm^Qi$;%Pds;r9mv3Svd?B)rjQ z90g#`?m|%4N->rv!@W<(QNDc4lid} zxvju+_iy>x%=Wdl{R@}COq2K-P6B+_8(mv2HVC*#=S!qQUy-(_#+uWy#DxaS2N^v~ zol1LgQ;yhLRhy}UAG_k-;wAQ4YNK#OTvwbk!BD*wn}D{`ep%?AMY>*tb`bmOe~8k5 zRwT~exVd!I6u?*3Tq4`U!uz=S?X0q2VQn})jLh){B}Oi@y9_{KQj}0r4w=b_m;FZ4 zUan>lTkSHlhnFBE@#sLA-cXbgI~&dT4e1FpwpzMrKkL2%X^)<}azsywZSgdUKFtLW&}W#%bDI@tLm zqEo>+$=N680ND;4e3Yzj`?QNhsG{{mxGvH+bxJPpO#l@xLS)d?#YS)Vq{;K+G=VP^ zyzkPv@Zq(Dm7YDE;URyee2^pt!n)e5OvGZ|$Gdd8b+=ChMg--FTt{90s5sB^TlH4i z$N!)XE(#6eZjgoTVpJ{GPn>mFxFgwEnMog8o?2F@TZso^`;Ixr;cwNvKf7E`MhTI` zVe+B%VUX>*HSLi3_wD7mxSN@cBBar=U*^-vh@v$egqvNd(*yYM>Ab}_RrOcq2`<*p z>~b~CFN8s_N?c^VlDA%|_S)pY@BA(dD6{!4rgKp`;y`fpF(9UPtESv~ImXEFPv(?d6X~uy@IMr&Yq&=C~D}z+uV&P41*7atWQm39&bKH)6XqBZX>E~oI zS#m0Qr*}FS`lM5}=Ug}F?3Xp7-FCS&;U*me;dYy7GREh!j3s$SJ;QxEexw#-e_zff z3sNRuarS-VTLzqZ|1Ewst())?-(Mw&g>gEDe0ppRH$%9HeRVK+Z^t7H(x$x>)6kKC zw$vU4G-9l~h-E&SmV=T=`Mfiam_+?~G_Q-+z0qvdJC-7pq6)3+>c$xmjvv8opoNB@ zM5FnP(w$oAjhcv_&2eD3LSxeG!lI#jAExR{g;m{3?KqA7vMe6ot|(Ps0Z$YBNp6Mg z*XkA{-X1cS&Rs7iYC6=pSL68^RR41@rzyY@_v{es zYit?Qb8F2o#>)jTGsD0BVR77(LB~*Vh5&T$ExA#NP;V4ddZc9KI1;$XRaRHpE*gHi z$~K(;R3&cvQdrEKAjw!-vI1TM4!g*oNwt40cO$Gf`PN*on;oshD(*)}V_>P1fnVwy zOv<$CWi!Ptuj}P9BwKr_6l6OonSohZm@`hl{M`ztT7P2)Wd4^^$nfhi zMt_rE4d$%;!?L+Jd<5VjSoQS&yB6~aMaM)q$fzMeMp@{Vrj~=E$8qHr7;DSx?zK1R z4=J9?=}^eF>5;kCbpx<5?e0Co z%7n_Gl0}*rF)w~(Z!j4)bw`b1GWV&1nwrV_n##cG4qul%z4zDGLFb9ceAu25I^O^l z99Pu>B8of6Y>+FKfBUnn{Jzzzl(RE<*pSzaiY;2B_kA81TZsM^4Up!SY4&uMsD8qZ zot0m1t$THxhMztWfux^ohNv}`v4+}${7=eYe|Gl+ZY3X_TEoSpGJl(1Ns`^g3E2&# zjEHlX+fuTafKL$~xNu0>S~D%^P@`CuAT*_EI74rv0Lte$VkY-hZ0t1fKAc zmjoBN_XMwR2Ev(Gex*O z>~TAZ7x2!5I%<&{hB<@2Br`giO-g}G_*15jv}fnIgkjw;u0fR+9#QXpt%t0M4FHyo ziZCmM!**k3+kxtq;u_;oCSEVce7s%5K}utV=%35LaNniOb@TC!W6ijwFgVr&{_^I> z)(i@BI0q-e1%D2QNVTKUdG7%y7kLJcZTJ*g_8uWirM#Y2lpciA?%4rdz5F(24jc$8 znGaK~=$lk|Ww9>pTea;&m`n>4_(CD8Wb4ZxVnaE{2(o+IN-m%`5t%H^Lv-n0>*%%d z70|mXLpOqa(eoypRb)q&?+siq4bXAuO~Iu7Kk|1tlink?qk?@3Z#rhSB@$A(p4Rh) zmua4%zT9jEU7D-Uyb{w{|Gb$hku7yojb9ll?8Im(XA5G$J%UyU+vJ@12vm96c(u^f z348k+JuDnq9I-fG_>)FYd&=&(%C@g{d54v2NI%l&e)~pqZkYmaqC6XOMzvDbTuV{~VIaHewCdA{lA9 zE$02Oa)k9wVBLdmDl2gzWVh>eLG=I=CJA(|K3MnQvmVOthbSIWZ-479cAao6BVflx z0_|ioOuuR$v_ct^*TVuHgWh6IwGHD!k(dW^GV%*{K!xIq-ag!LuDH%eC+aw_=T6?c zh>nlnUcD==TDl;67#*ZHn&Xm;upMt-c||z)(PRg~C|$Fdf9N3Y{A)fAD((la!0PKyKKh(<(jwYs7rDeOU=391 z{gsy!vo4^1LD`w*wqJ{sNvLGkR0o7IhaSqCQ*%75rUn1`?Vp(DIeFAgo9NI^+&_vp z3hHt&QYA;yHq?V%08c*18BSU74MEoJIxC0MJAmJ^jLoxRbZFHVq%4@%DMX;-EI1a} z@~e-FSi<{>h6Jq~=O^m_ic zNk(B8fH6q5qgGL$fNXD5Q7)t9kfV!8xsXH;`rcckJ$n%3*YkF zOFlqg-8ktb#(JW_Ndf{N{V&F0c-$vPZ|hUq10#uo7HoBEH-vWwVnalJ&_}o|DDAwe zfOeiwl%<>=j>=e0Wluz#AWSLz<)C|syV};$E;4k`BR%4{JUp-+F3)ObKU~fd9W7@- z*l)&4kM>t}w?KSG?(E4TLJ&aXvBix|wA94_Ipm>Td*K}}1U20nggztLhu zzGb&}@NhvPZPowsZlqe3Af>VbWY(M-=#1)iCqKw~S8OpBME)Z>@I|8FrB!X`PI2(h zKOJ%^4-T7UA#eq*EBJ9tuMaKQv~fMo!lmiG>6C@4Ordkkoaf7dHEJ!HEZiNWVu9K1 z8HD*!1KsJTn^o=g5|2R(Ne>Pd+TAlQRCarPCXv=iHj(6v@&Wf4r_NdSF~1X@l>nm^ z#qfryKAP{Ur>pmLTXcFrMOW1uZyP^s4}?vd`H#JjJK_IyH~(aeUhz3v*Q{%ZCwy`^ z-%#lN3-o5^%o0bBhFze(ZCb>!MCybu-Yh!ipOau+G_;dC6{Nb$2R$U}Au#{4)-#%% zzu%xVKJCpz$Kns${IHTLct%Dy)Jkc0v1NJnsnOTI?f${lK%R3=udGv!5cQ$J_I{FC zf1(1ObabbEZZdSnRIPI&*Drfa#+H)ny5mTpnQ*#RC(h!Q(rlKLSPP{Glq*I{kXV0u zYKkcnE4D{k4wEpz^yKR&@|i|XImd8;WGCv}hfAUUJPb80{f1nT!c~h)&fw7%-<>U{ z6vEw7sD%Sl1MAf8<=>~(yLlDtjV>a4LheyzJk-d~)9hq}XHObL`d;VY4mPumK2mob&%?j{;{F!vf(?=ap>SPv?dz@yoxIZ3S{&OtuT8Cn;x}V$5 ztlPe@=z(<`v0fUzoBJM3#J=OuP*3a5taOZIvlU+o(~L*xf#9Oa8Ggs2r~Up@7YEFv z_DZJvXlEq~9Q|?Y6}p`#ly^5bxH;qo9_ZYkui#SAFY68C+jRB$(==c>E9zxGw2{(w zi#r#`J8<^N^{qpQ>PJwKBc+_Qr8Gy)_zYE4qwBhs;^U?rrZ5Y;D-;&fT)8fT0{TEJC7^V5$o1;dVwBr`4=8X)um3|*-GX}Vd!qah=aX%3%S{4;tvC{2!gc*h zb*0tD?&Si?DZ_7X5TRw7ij3A|GgP%L|H|6-`JttB-Tf~L!VP7s)l-$yd3QCUwBYrBGD=bph8lvI*;_9Sr=7EVxPhd!l&Z!KbON?ai$O zL(M)8Nx(H&hsdYRFDA&u@=@MTl||$g%&?iW4yp*Jlo6tock;0{xkPk7E76QeRN4Vh zg4A^HD;4E;FpGF;C3V>fWQme@sG_eap02+4f20tK*75(+M+or$5lztPKQ8uO%PQT7 Sic$Ql4C(6_Yh&*_#Qqx_@)y?t literal 0 HcmV?d00001