From 67a394a7f4ccde1d50f5646093eb6175045bc24b Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Tue, 27 Sep 2022 22:01:57 +0200 Subject: [PATCH] Podcast addict remote initial release add shortName Add Podcast Addict Play Store Link Podcast Addict use official icon new layout changes to README.md and metadata description requires GB 71.0 screenshots --- apps/podadrem/ChangeLog | 6 + apps/podadrem/README.md | 21 ++ apps/podadrem/app-icon.js | 1 + apps/podadrem/app.js | 372 ++++++++++++++++++++++++++++++++++ apps/podadrem/app.png | Bin 0 -> 2120 bytes apps/podadrem/metadata.json | 18 ++ apps/podadrem/screenshot1.png | Bin 0 -> 1836 bytes apps/podadrem/screenshot2.png | Bin 0 -> 3220 bytes 8 files changed, 418 insertions(+) create mode 100644 apps/podadrem/ChangeLog create mode 100644 apps/podadrem/README.md create mode 100644 apps/podadrem/app-icon.js create mode 100644 apps/podadrem/app.js create mode 100644 apps/podadrem/app.png create mode 100644 apps/podadrem/metadata.json create mode 100644 apps/podadrem/screenshot1.png create mode 100644 apps/podadrem/screenshot2.png diff --git a/apps/podadrem/ChangeLog b/apps/podadrem/ChangeLog new file mode 100644 index 000000000..c26e40c0e --- /dev/null +++ b/apps/podadrem/ChangeLog @@ -0,0 +1,6 @@ +0.01: Inital release. +0.02: Misc fixes. Add Search and play. +0.03: Simplify "Search and play" function after some bugfixes to Podcast +Addict. +0.04: New layout. +0.05: Add widget field, tweak layout. diff --git a/apps/podadrem/README.md b/apps/podadrem/README.md new file mode 100644 index 000000000..3760e6b5b --- /dev/null +++ b/apps/podadrem/README.md @@ -0,0 +1,21 @@ +Requires Gadgetbridge 71.0 or later. Allow intents in Gadgetbridge in order for this app to work. + +Touch input: + +Press the different ui elements to control Podcast Addict and open menus. +Press left or right arrow to move backward/forward in current playlist. + +Swipe input: + +Swipe left/right to jump backward/forward within the current podcast episode. +Swipe up/down to change the volume. + +It's possible to start a podcast by searching with the remote. It's also possible to change the playback speed. + +The swipe logic was inspired by the implementation in [rigrig](https://git.tubul.net/rigrig/)'s Scrolling Messages. + +Podcast Addict Remote was created by [thyttan](https://github.com/thyttan/). + +Podcast Addict is developed by [Xavier Guillemane](https://twitter.com/xguillem) and can be installed via the [Google Play Store](https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US). + +The Podcast Addict icon is used with permission. diff --git a/apps/podadrem/app-icon.js b/apps/podadrem/app-icon.js new file mode 100644 index 000000000..fc4406666 --- /dev/null +++ b/apps/podadrem/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgHEhvdABnQDwwVNAAYtTGI4WSGAgWTGAYXUGAJGUGAQXXCyoXKmf/AAPznogQn4WCAAQYP6YWFDB4WFJQhFSA4gwMIYogEGBffLg0zKAYwKRgwTDBQP9Ix09n7DCpowBJBKNEBwIXBAQIsBMwgXKIQReCDoRgJOwYQDLQU/poMBC5B2DIAUzLwIKBnoXBPBAXEIQQVDA4IXNCIQXaWAgXNI4kzNQoXLO4wXLU4a+CU4gXR7ovBcIoXIBobMFPAgXILQKPDmgxCR5omDc4QAHC5ITCC6hgCC6hICC6owBC6phBC6zcFAAMzeogALdQjdBC6AZCeYfTmczAwfQhocOAAwXYgAXVgAXVFwMAJCgXCDCYWDJKYWEGKAtEA==")) diff --git a/apps/podadrem/app.js b/apps/podadrem/app.js new file mode 100644 index 000000000..a9b5514b8 --- /dev/null +++ b/apps/podadrem/app.js @@ -0,0 +1,372 @@ +/* +Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"", package:"", class:"", target:"", extra:{someKey:"someValueOrString"}})); + +Podcast Addict is developed by Xavier Guillemane and can be downloaded on Google Play Store: https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US + +Podcast Addict can be controlled through the sending of remote commands called 'Intents'. +Some 3rd parties apps specialized in task automation will then allow you to control Podcast Addict. For example, you will be able to wake up to the sound of your playlist or to start automatically playing when some NFC tag has been detected. +In Tasker, you just need to copy/paste one of the following intent in the task Action field ("Misc" action type then select "Send Itent") . +If you prefer Automate It, you can use the Podcast Addict plugin that will save you some configuration time (https://play.google.com/store/apps/details?id=com.smarterapps.podcastaddictplugin ) +Before using an intent make sure to set the following: +Package: com.bambuna.podcastaddict +Class (UPDATE intent only): com.bambuna.podcastaddict.receiver.PodcastAddictBroadcastReceiver +Class (every other intent): com.bambuna.podcastaddict.receiver.PodcastAddictPlayerReceiver +Here are the supported commands (Intents) : +com.bambuna.podcastaddict.service.player.toggle – Toggle the playlist +com.bambuna.podcastaddict.service.player.stop – Stop the player and release its resources +com.bambuna.podcastaddict.service.player.play – Start playing the playlist +com.bambuna.podcastaddict.service.player.pause – Pause the playlist +com.bambuna.podcastaddict.service.player.nexttrack – Start playing next track +com.bambuna.podcastaddict.service.player.previoustrack – Start playing previous track +com.bambuna.podcastaddict.service.player.jumpforward – Jump 30s forward +com.bambuna.podcastaddict.service.player.jumpbackward – Jump 15s backward +com.bambuna.podcastaddict.service.player.1xspeed - Disable the variable playback speed +com.bambuna.podcastaddict.service.player.1.5xspeed – Force the playback speed at 1.5x +com.bambuna.podcastaddict.service.player.2xspeed – Force the playback speed at 2.0x +com.bambuna.podcastaddict.service.player.stoptimer – Disable the timer +com.bambuna.podcastaddict.service.player.15mntimer – Set the timer at 15 minutes +com.bambuna.podcastaddict.service.player.30mntimer – Set the timer at 30 minutes +com.bambuna.podcastaddict.service.player.60mntimer – Set the timer at 1 hour +com.bambuna.podcastaddict.service.update – Trigger podcasts update +com.bambuna.podcastaddict.openmainscreen – Open the app on the Main screen +com.bambuna.podcastaddict.openplaylist – Open the app on the Playlist screen +com.bambuna.podcastaddict.openplayer – Open the app on the Player screen +com.bambuna.podcastaddict.opennewepisodes – Open the app on the New episodes screen +com.bambuna.podcastaddict.opendownloadedepisodes – Open the app on the Downloaded episodes screen +com.bambuna.podcastaddict.service.player.playfirstepisode – Start playing the first episode in the playlist +com.bambuna.podcastaddict.service.player.customspeed – Select playback speed +In order to use this intent you need to pass a float argument called "arg1". Valid values are within [0.1, 5.0] +com.bambuna.podcastaddict.service.player.customtimer – Start a custom timer +In order to use this intent you need to pass an int argument called "arg1" containing the number of minutes. Valid values are within [1, 1440] +com.bambuna.podcastaddict.service.player.deletecurrentskipnexttrack – Delete the current episode and skip to the next one. It behaves the same way as long pressing on the player >| button, but doesn't display any confirmation popup. +com.bambuna.podcastaddict.service.player.deletecurrentskipprevioustrack – Delete the current episode and skip to the previous one. It behaves the same way as long pressing on the player |< button, but doesn't display any confirmation popup. +com.bambuna.podcastaddict.service.player.boostVolume – Toggle the Volume Boost audio effect +You can pass a, optional boolean argument called "arg1" in order to create a ON or OFF button for the volume boost. Without this parameter the app will just toggle the current value +com.bambuna.podcastaddict.service.player.quickBookmark – Creates a bookmark at the current playback position so you can easily retrieve it later. +com.bambuna.podcastaddict.service.download.pause – Pause downloads +com.bambuna.podcastaddict.service.download.resume – Resume downloads +com.bambuna.podcastaddict.service. download.toggle – Toggle downloads +com.bambuna.podcastaddict.service.player.favorite – Mark the current episode playing as favorite. +com.bambuna.podcastaddict.openplaylist – Open the app on the Playlist screen +You can pass an optional string argument called "arg1" in order to select the playlist to open. Without this parameter the app will open the current playlist +Here's how it works: +##AUDIO## will open the Audio playlist screen +##VIDEO## will open the Video playlist screen +##RADIO## will open the Radio screen +Any other argument will be used as a CATEGORY name. The app will then open this category under the playlist CUSTOM tab +You can pass an optional boolean argument called "arg2" in order to select if the app UI should be opened. Without this parameter the playlist will be displayed +You can pass an optional boolean argument called "arg3" in order to select if the app should start playing the selected playlist. Without this parameter the playback won't start +Since v2020.3 +com.bambuna.podcastaddict.service.full_backup – Trigger a full backup of the app data (relies on the app automatic backup settings for the folder and the # of backup to keep) +This task takes a lot of resources and might take up to a minute to complete, so please avoid using the app at the same time +Since v2020.15 +com.bambuna.podcastaddict.service.player.toggletimer – This will toggle the Sleep Timer using the last duration and parameter used in the app. +Since v2020.16 +com.bambuna.podcastaddict.service.player.togglespeed – This will toggle the Playback speed for the episode currently playing (alternate between selected speed and 1.0x). +*/ + +var R; +var backToMenu = false; + +// The main layout of the app +function gfx() { + R = Bangle.appRect; + marigin = 8; + // g.drawString(str, x, y, solid) + g.clearRect(R); + g.reset(); + + g.setFont("4x6:2"); + g.setFontAlign(1, 0, 0); + g.drawString("->", R.x2 - marigin, R.y + R.h/2); + + g.setFontAlign(-1, 0, 0); + g.drawString("<-", R.x + marigin, R.y + R.h/2); + + g.setFontAlign(-1, 0, 1); + g.drawString("<-", R.x + R.w/2, R.y + marigin); + + g.setFontAlign(1, 0, 1); + g.drawString("->", R.x + R.w/2, R.y2 - marigin); + + g.setFontAlign(0, 0, 0); + g.drawString("Play\nPause", R.x + R.w/2, R.y + R.h/2); + + g.setFontAlign(-1, -1, 0); + g.drawString("Menu", R.x + 2*marigin, R.y + 2*marigin); + + g.setFontAlign(-1, 1, 0); + g.drawString("Wake", R.x + 2*marigin, R.y + R.h - 2*marigin); + + g.setFontAlign(1, -1, 0); + g.drawString("Srch", R.x + R.w - 2*marigin, R.y + 2*marigin); + + g.setFontAlign(1, 1, 0); + g.drawString("Speed", R.x + R.w - 2*marigin, R.y + R.h - 2*marigin); +} + +// Touch handler for main layout +function touchHandler(_, xy) { + x = xy.x; + y = xy.y; + len = (R.wb-1 instead of a>b. + if ((R.x-1 { + if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup"); + } + ); + Bangle.on("touch", touchHandler); + Bangle.on("swipe", swipeHandler); +} + +/* +The functions for interacting with Android and the Podcast Addict app +*/ + +pkg = "com.bambuna.podcastaddict"; +standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver"; +updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver"; +speed = 1.0; + +simpleSearch = ""; + +function simpleSearchTerm() { // input a simple search term without tags, overrides search with tags (artist and track) + require("textinput").input({ + text: simpleSearch + }).then(result => { + simpleSearch = result; + }).then(() => { + E.showMenu(searchMenu); + }); +} + +function searchPlayWOTags() { //make a search and play using entered terms + searchString = simpleSearch; + Bluetooth.println(JSON.stringify({ + t: "intent", + action: "android.media.action.MEDIA_PLAY_FROM_SEARCH", + package: pkg, + target: "activity", + extra: { + query: searchString + }, + flags: ["FLAG_ACTIVITY_NEW_TASK"] + })); +} + +function gadgetbridgeWake() { + Bluetooth.println(JSON.stringify({ + t: "intent", + target: "activity", + flags: ["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_CLEAR_TASK", "FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS", "FLAG_ACTIVITY_NO_ANIMATION"], + package: "gadgetbridge", + class: "nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity" + })); +} + +// For stringing together the action for Podcast Addict to perform +function actFn(actName, activOrServ) { + return "com.bambuna.podcastaddict." + (activOrServ == "service" ? "service." : "") + actName; +} + +// Send the intent message to Gadgetbridge +function btMsg(activOrServ, cls, actName, xtra) { + + Bluetooth.println(JSON.stringify({ + t: "intent", + action: actFn(actName, activOrServ), + package: pkg, + class: cls, + target: "broadcastreceiver", + extra: xtra + })); +} + +// Get back to the main layout +function backToGfx() { + E.showMenu(); + g.clear(); + g.reset(); + Bangle.removeAllListeners("touch"); + Bangle.removeAllListeners("swipe"); + setUI(); + gfx(); + backToMenu = false; +} + +// Podcast Addict Menu +var paMenu = { + "": { + title: " ", + back: backToGfx + }, + "Controls": () => { + E.showMenu(controlMenu); + }, + "Speed Controls": () => { + E.showMenu(speedMenu); + }, + "Search and play": () => { + E.showMenu(searchMenu); + }, + "Navigate and play": () => { + E.showMenu(navigationMenu); + }, + "Wake the android": () => { + gadgetbridgeWake(); + gadgetbridgeWake(); + }, + "Exit PA Remote": ()=>{load();} +}; + + +var controlMenu = { + "": { + title: " ", + back: () => {if (backToMenu) E.showMenu(paMenu); + if (!backToMenu) backToGfx(); + } + }, + "Toggle Play/Pause": () => { + btMsg("service", standardCls, "player.toggle"); + }, + "Jump Backward": () => { + btMsg("service", standardCls, "player.jumpbackward"); + }, + "Jump Forward": () => { + btMsg("service", standardCls, "player.jumpforward"); + }, + "Previous": () => { + btMsg("service", standardCls, "player.previoustrack"); + }, + "Next": () => { + btMsg("service", standardCls, "player.nexttrack"); + }, + "Play": () => { + btMsg("service", standardCls, "player.play"); + }, + "Pause": () => { + btMsg("service", standardCls, "player.pause"); + }, + "Stop": () => { + btMsg("service", standardCls, "player.stop"); + }, + "Update": () => { + btMsg("service", updateCls, "update"); + }, + "Messages Music Controls": () => { + load("messagesmusic.app.js"); + }, +}; + +var speedMenu = { + "": { + title: " ", + back: () => {if (backToMenu) E.showMenu(paMenu); + if (!backToMenu) backToGfx(); + } + }, + "Regular Speed": () => { + speed = 1.0; + btMsg("service", standardCls, "player.1xspeed"); + }, + "1.5x Regular Speed": () => { + speed = 1.5; + btMsg("service", standardCls, "player.1.5xspeed"); + }, + "2x Regular Speed": () => { + speed = 2.0; + btMsg("service", standardCls, "player.2xspeed"); + }, + //"Faster" : ()=>{speed+=0.1; speed=((speed>5.0)?5.0:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});}, + //"Slower" : ()=>{speed-=0.1; speed=((speed<0.1)?0.1:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});}, +}; + +var searchMenu = { + "": { + title: " ", + + back: () => {if (backToMenu) E.showMenu(paMenu); + if (!backToMenu) backToGfx();} + + }, + "Search term": () => { + simpleSearchTerm(); + }, + "Execute search and play": () => { + btMsg("service", standardCls, "player.play"); + setTimeout(() => { + searchPlayWOTags(); + setTimeout(() => { + btMsg("service", standardCls, "player.play"); + }, 200); + }, 1500); + }, + "Simpler search and play" : searchPlayWOTags, +}; + +var navigationMenu = { + "": { + title: " ", + back: () => {if (backToMenu) E.showMenu(paMenu); + if (!backToMenu) backToGfx();} + }, + "Open Main Screen": () => { + btMsg("activity", standardCls, "openmainscreen"); + }, + "Open Player Screen": () => { + btMsg("activity", standardCls, "openplayer"); + }, +}; + +Bangle.loadWidgets(); +setUI(); +gfx(); diff --git a/apps/podadrem/app.png b/apps/podadrem/app.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cdf4fedb180793d3dcce293c65284a9eeca7f0 GIT binary patch literal 2120 zcmdT`iBr=_7XC>Dl?;g2a%{<l z9aR7T)R4}I3yRfrYwiRoe9+vk4gdfFZZ4i@ovL378dqr{59ZTg)0JfB8;B} zSNQ)0a8isF09O2e6aT8iNj6r%*h$8m)Ge^WzOw#?`J(gJ>{pC}rPySDQP?7W2{;O= zubRnQ+0D&ORve5O17k!&*K&-RQP3?`4xAYaTg!$jYG%j7n9)#0k*ru4I{~J+i10a? zooKuo0FjG@GCq@f39>1bZpvj03{G%? zfrcno$bQE24D3(#LIxon4e^<)s6R~%oM~rrIv=Vl2Vt&lP{PV7Z~w&!e2ty ze?p3%=C9>j%lRxpu^ktWV#OMA@=!uT5WhNx5oRD6Zxwgmpoc=_JgS`gnReMo_>3f3 znPFvl(;}^z*Dp#x&GGJ~u8~W{e+&qR+i9UkxfKcQf?zh@pL08s-*A(af#MOe)_yIN z&JD@gAJ^}BNatS&N*rX&C3Yrqz54<8cF(5V(Z~G>BN}O8#+h;QFNlU3_|>t} zl`($td2v^Pbh>Bbbptc)d)|W|8L^gvLR-ngxMZ?Z&ZqJ5w(B*1lKEGh8=k_E4rbg* zI`+Gb#Yxd?%EruZpS^UMX)f`K_E&`~ntvaOOa*{#K;i|I2k?!&kU#`Y*FP33S9O~Z zyOG?om8F%u?%ucI*B(j(BP^rLI_sRCWb)KcLFi&|ZJX$4G*t?#xsRJO5C@bNDBAG< zcmPleNKf%D2$2YTPv_w)K>*15G$zkDUCV>`xd>$9}qH{P`dE(-2) zdv?{$NTnp-{x(vr2`{5PHK{Ff|)+09%Pcm z(JR#xFGFAM$O@LEeg9pR$`h8ao0^GsZGrlUvo)GY zI9#flS%XH66_XssO%KutE!w`n1oJ;Q=(oMIF=6^oOeKHhAPwt5e*C(+fJQQAr@C?^ z9>9!1jnCbRG6HSXHb883nn1P7=KhWW$F52!R9g!1x}eIixYA{k|I$k8`X|}XgB0Q(*n45plb&)c&0n_(;g(!z`(F*Fd;JaUof8xz#+#^sM3%p z?#uR0Ka;%5ksFktcB`%fU+mn+)J%Y+E0_=O2FI3GFtW2o=;n4PD*%$JLe=}%WqBhG@aIP z&6%wqO9-n&8^N>D6Q{Mf>$r*dyj zl?Jg$+ak=R`eLOjDi(voQ7DvshiJCX%C)slnnjeSy_2T9C1*W6?<{hBJM5=KcZVl= z^?a)?umh95y97slCh1Un9i>5^#MEH-*oWJqRagBgFVi>!iN(8)cX!l|=)cM()=g=j z?P>0?{Wd-!Z`8kLrb1b{q0Ip}qtRe9J!AKS63}7$_=Sw6w@dLu0FY-<2$Dlk_J05* C9lC@7 literal 0 HcmV?d00001 diff --git a/apps/podadrem/metadata.json b/apps/podadrem/metadata.json new file mode 100644 index 000000000..929269762 --- /dev/null +++ b/apps/podadrem/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "podadrem", + "name": "Podcast Addict Remote", + "shortName": "PA Remote", + "version": "0.05", + "description": "Control Podcast Addict on your android device.", + "readme": "README.md", + "type": "app", + "tags": "remote,podcast,podcasts,radio,player,intent,intents,gadgetbridge,podadrem,pa remote", + "icon": "app.png", + "screenshots" : [ {"url":"screenshot1.png"}, {"url":"screenshot2.png"} ], + "supports": ["BANGLEJS2"], + "dependencies": { "textinput":"type"}, + "storage": [ + {"name":"podadrem.app.js","url":"app.js"}, + {"name":"podadrem.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/podadrem/screenshot1.png b/apps/podadrem/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..e028b331824cd45a6217ea520e68e91442e09215 GIT binary patch literal 1836 zcmeH|TT@e46o5|-2MC}{LIA;lC{aXip&$s-0LBmu6_m>i?P%a+P+^*ei^wGe1EEj_ zlTbzkK_wLw%hUjMFfd3kD1~w{kxmd50$9tXLQIC(q(}%ooxb)Tv=4i)J$tRS*ULBG zt~?nRz1@1JH2?tHX~!rDNbLN3p)HWTqx-iMB%pE=qRBwJ+~s!wuvnx~jwBVPs2krD z&3Iye0d(|{bTRq`Dgky zM%PvgA5++6wxu&>|7?WG&$XGee+(qG1yv1xw>O6X( zV?`XxI#K*yzF_W`e3DJAYILTTwC5$6Zk4{nN$wnez4gY`iD$rEWb>GFF)a>`^tq$e z>~pWCiIdnn)MFAeg7#B8A^$=skLWbfJ-$p@Tkf8yY3*qexFX~BBx!N5r9QB= ztOfw_Z|v0jOJ&@lB!9a|m~gsz6T-YyT(_ozNY`gwA^_ZQP8>J}@C4bFdR!zNk>=%O zwYwARH^BwbtLewh2`DJ|$-dVo0Ny6Q4c=QSqxMiXkP6W!4t(S(bbUqy3i>Y(bCHo0 zy6UiS5%J7pZu82}EcT*{B}mXJZBRVCd4K*ab6Bh%l!jesNFEct4tR?&$rBW+SV$7`9K;0!U}uO2<* zUTa(2fv;h11wF{N6p}fXZBO6Q$gt~F$|3iuOe+`Zk}}rdN)wkH_(2+bxrxY@EcSgZ zOt&u>+^nC@aVasC42(KOR~5FZX#$9!zr4(bOpme%nHACKY7b2;%b`<0Vo#du1!Bk# z4bL_E8Uh0#zmRCKj#c@-gI+DPMbAh8Opm*kKHz?vvm^2p-XOI>(Ef4*8xlfR=8rEE z`tx51bQr<|4)j)LUraZ!6vkmHlRUI@8Mf%lrf z1wb2y-8BSHT|;6#!f}Ovv%_<}tPz4S z9`WlCCPWnKPyt~+M_>KE82o>n4Wll8iEa9Ga!FUw;D%^5|Kz5e0uyF58BlNl6)^~; ze;S2oA|EECA!cBycw-(Pg6|r*TeUi`@q-5D{T;XN^%v>`YzkmVjx&wKOk2Ce_T`~I zwD;}5Ax%(U<|yZ`L^0!7=~?2$2X3>xmM3Uqrs~vBSA>q@&}u7V{ra_fT@Zr_jGAM3 zMcuYKVY>(1g^Jo_O1YJ;zU9+l{$Tf*bC8sDKqF_F@1irL-hLf-4c^Z@v(-NMXZzlz zYW`T}uG{iuc@vx)IVB^@O{)&UjcLfOX&Ri8d$xe}-X(Vfv$Fy(?3C<($hE5e7|@6# zrQWD9`cgtCGNL+svL8_uD(6ZZJxiThi=#jG)#hfj87;u9Eqkki`9qOswkfcY^Gh!i z*=FFX41bWlaKK$Vs#3w3=)NOYlbhVZjv3YV#c%#4^=pLVL@kL_HoJs;e*i5ij?zws G%l-msh8pqMA%$q zb1Eg&Ge*J2m+zMY4x7zBa`~nvl!99w7L*}+@)WB#gX&stN$rdi_?R&=3PSFY#MNh- z!BUemGWya@)c7oh=3EDIwqxn&8pDNoSF~%YsMMWv43Z5YY0jxeF%y>3xVrXfS6*%9 zaz!qCC3-n{Imh!&6$Of3;)xOo^*q;ujp9F3g6L0 zHZhA;hc--O2l9<=Yv+SgzzHayapQUkP;-KrHZl`P^?L z4zmt_m-?GS~qIFCt<#VmWW{5H~}un)p!){ScxF#4wk$1Z`&8V{la7Y zAG7RDUm@s7TskGc@jh!Zez9Yc%_P^CnlbhRH{Rgv!+^E$#S6f`Lyf@2PP#%9`gLWJ zg-|Nx;PL7NQi_=_%&j`l2W|dAgFmamJOQp?$D^PL7UZ^GJ)g}Z%b4E&~x zn&)kex{Y^waROk?6jwlzcV`%oK_uC)13 zWIEKe>Qc~S<4(j2ht>7ApVh3}_x8gqrw1F*Mh`ZQLXV;bbNrSZqPFGslaq@@J}nfs zmYH)`K=S21&xkB&#c*Dr3`ShK3WJwj9#y&Lh|tjN5P=68dfK|Ygke)%SsSJv~Dx}pujGZ@szty zTwi(ogsiiRnJ48_#9p;L_o(+tJMNHg+^_fJjJ|gpT)s|&??!;-LCFo0J#OmF`cLoaK{*9Y>pP{#1R3)txh_z}vJuoj zQy~wDr#b%RMorPBO$xH%F!%c4W3r_aobeaJrz7$l26aTK72{@&JjIeQ6$6gCbUg-A z(JEH(knF|deY3yz6hz7|c=`}_uW)(;+vz3CagF?8V`E+tZ`JF${w1rBHs|gg@jtRK zSr)_>|3{dX+xO|UcG-lVtYADRfwaRPK(VLOb|rave2?X>bXrJlj1b~D?-lv67l0KI z)_P2xW-VyryfCmdn{uFZPVI(5q2&*880iYPoAD^h0I=r%5dC1a00^8?{(V0gN9hgQ zEpG9=wuxt7ijC|o*6(ewmh1vqZF3Q{I{0J@17Kx*iw~wFJL?K0uFj=bP~S*Ocsn5;xNxUS|J*&fV`2Te-~ zr(4)SXKj*nOOaMK9J|Pz_5QzAIHgLOdX38WLmIO2B)gYj{tmuR;~7SG4QlKP>B)n8 z%L}zbG2>r!fR#0LJXsyaX#JxK*W@Y$Zg@a@q@4KVAS}*luRJd4CkRm`A6GKDcX|uu z^b>2{8a$nF&DN=`7G8+iBZP$PL?dY?EI|wc2%9Y zs@uwCqX$NtKUZP0L_9_t^(1vdXG$|83R*l8QzCBkhqXfB6Ro1(){|vL>coFd>|5U$ zjx!;)SA<sga6(5Ny^gamYpJ;ncW>;Sog9Rzt7S1uxzxswT6LEzc(lk8Ayuw!4&1y%g+%}VcoNrNI}D5aT6;9{5v~yeYb3;(__9rQ zJ7_J2cM?r>7mCMOb=23K6m>n=s4sn_3-?0RG7o(!+Bz@3k2i`{h9bY3*Yav0%-5v6 zwD}y_SUro9((vhhVB|*VDx7CD*mFX6%X^b%Pc#nm&b*~wZIHPG{(Cr6rqLg97a=TdvpW>!Ez1qtNN7dX1dsDkF>`oD%qIEOhQClIzu{!;Z)4YlayOR+W z7*@OOmh0}KwsOlDp%_Q1pAI9~n%)tjW-#GO>~>f6zq}iD?7b6rEEl z^rCILesr{Gy6zSQW~3Y#ll(hMOFLKtIIG0}#O8*Ys?MSv8Ch_A)aF!bh;mi=FasE9 zd03si%KT%6dRWle9#!Dvw*Gq!D2DZsFnsEgCrV1%XS*$VP9h19-XV)z=@3bw8RL;< zf7Xo@+t`UrC(Tj~%>1)*=fYXLAto_Cc=V)*GT}pvP8cGj#QB&u-IE=0>_#ISUtG;7QOkIaC<|oP-c32gWgAjnRwO7-}Y*d@}&HFh^*Eh5sM_ E1z;gAOaK4? literal 0 HcmV?d00001