From 0dafb3fe809570fed80f0e0fb06511a231201748 Mon Sep 17 00:00:00 2001 From: Andrew Lorimer Date: Sat, 1 May 2021 17:11:33 +1000 Subject: [PATCH 1/1] bugfixing, obs interface --- icons/ppt.ico | Bin 0 -> 170879 bytes icons/ppt.png | Bin 0 -> 12417 bytes index.html | 4 +- obs_ppt.py | 209 ------------------------------------------- ppt-control.js | 34 +++++-- ppt_control.py | 218 ++++++++++++++++++++++++++++++++++----------- ppt_control_obs.py | 144 ++++++++++++++++++++++++++++++ settings.js | 9 +- 8 files changed, 349 insertions(+), 269 deletions(-) create mode 100755 icons/ppt.ico create mode 100755 icons/ppt.png delete mode 100755 obs_ppt.py create mode 100755 ppt_control_obs.py diff --git a/icons/ppt.ico b/icons/ppt.ico new file mode 100755 index 0000000000000000000000000000000000000000..e3fb47b626724ff19755ce57a4ff89adb53cfecf GIT binary patch literal 170879 zcmeEv2O!nm|NpV~CNnGLT3uw%n_Vg+rC~LM5~9eygfvJ|T1HB=Xd20gqR46(*$I`b zkZa!mIhWh6=Xt)*^BvFc|GatM^Z9(vdB4y5yxwQu&*yUipa7geRFoZt)Zh&z0E;L9 zU}v8_$G~G&Y5;IJ0ORQaW*z`4GyurU&z`5V0$?Wyp#0o9Jpi=QNPhO*8K%up1SFyW zq96py%~J!w!2!jKg7i25;++97H=jL!;{<^2GyoaNg%12NFZtmIh z99jVEV<_kB7X{9+?7Z<%A1r|=WE^CK)2|5+0Q?7d{A>U};UQ!^C@Ly~5k%5IAQI6L zWcY_Jp^x81^!s9CPvSo%rB^4>rwg%jGs$y~fzY%GzKWKRC%H}UE)J`x%qI~N}d8=C5F?#LJk*WyKzYp(s^_qYkm~xsx1K}S zApGc%M`8d#l~7%Mw*GO&!g>XXUtKLW$%|B3)OCGHl_WL^dY3CU7M>H-`A(jM@q}3Z z0|(S~iQ}Ymq>lfv2#Bd21q(FeA)f>b zw9Wtt?L@%6_!{IpfLlHb@F?bhc}jNyukt;>r;-o&)Cz!v&N;AXX&P9hcNs|PUjrC} z3?OBA6G$1|0n)}fKxSDUmaKuEg;28I%E8)B8YnMG25HDFt#X9)bl%kAQ^nBd~DUBOtk=9AF^Jt$G9&n>_(o^D3Zh zSq3z$i-5}NM?h=iBY?H20rD14fv&?-puMRD;Owfv@@-YXa>o;3=~@YFyq*C4O|?LO zYc0TSdI45C*8;t*O<;}tGhppe1FUyF2OGSefpwnG!DinVK!001FmUPw2F~5UX!~1W z?D`&T@OuMn1N*_wkjEf6x(s;is{p&gDu8Er74V3t1KtN}fk$`)@IKrEoFfK+SM&gI zIXnTjN6!HNGgBbo>vx@&*8itV2=CJWDoOSk_n6}YdZzC6SIwu1FvrvLtU{cmW{)%;A~OT#M$ zzf$0${3WTVX#PmW&r#SW`X$NADl7jX1x|cll7QfRQQ1GDz>)C_Vq#*N$0sPNtn#B0 zXzlnB2sB?2Jv}2Mui#G%(E5QqHO1FNNlDKH<$r7d*$b)?khl?X%@GF|n(MH0z$F$d z9iOP|5AD~|nym-wNL+&Zvj!-Q{z&3b1Jlm{Kno;DByo}gqyk#N4W*$0TALGE`Jn|3&8=+fC`Wazsz52IkI00{*j5S0UCh{ z!Ntz?tMZc&z(}A!GC>7o7+@h#DSlce_K{WtKVX+u26vrU> zWy_YW3Pcvjeky~ww%Mjln}13GD+T|M3M3Ef0!G3M9@u`W)4wT?MbZ!igdYjW@mZS9A8MGDhvFm+zf&HAMe-zHqJv$eCWQ*|Yk9xV`6K&Ru3Wwx zi4&KhDoHZox8)TTNd-XiWC}$<%xd_xJOIjAMJ!3f7xJOXNPgCY-;&27d90!W)^6V% zd99qQ(lC&a`lY-(+^bR{Wsj5~Le&`&GJsUA#SMPpzhEe@2(3r|B=OHWNb;~_SfKL@ zdD#D=>=BWQLDm85%LNRspRrvsnEzc_a=MC?XR`UYJASlMNUwR%i9< zpUFe;zac9NOA=Ykk);Kc{+T@V{dYu4{@urq?f*^~UHDEGUHGB=55<41;79U55&xlr zAISey{6`ADm;Z%00BgPl;amA1A^)XDzi$+LBmceMzZ51N+kd6uD|u3-{Z8P!3l3js z_(C4FxZg>;KjA=@clZCDqMwqvkqUz3y#qd7{O90H1js|qFTz2fONzzAU0Fy|P#lE> z7zV?z9fl$>EP^2$3}FDMh(x%F3wIu8lOq9?90`!aLDIP{o)iF)+2B(McQ8pr4&kmQ ziO3<^6-D%Ih;}s*{ky|&^ZoXmY$rJ+mxCON;7Mje;O21ngbRWf;YG%ej3*HfFCzyO z!4Tox4nq+bB0z&-2nA-h$>;n8BhcmFAhb^`57`0oZ|xHcVts%hb}tZ;_XiRxexyBO zag8|AzOcZO^H2i-+}NM*2`|)50gG^{pZ0?>2G@a5EVs; z{1j~QuLF(&&%w5!2Dk_O3hn`S0z((L54__YX&>0kqZ3%}=?8W}1HdD!0eHfFUg!OT zz~#_5-0vMH?e}^eCxYE4h#=t13nj3%95SzKQvl39o=2s#%!A=Mg8yg$zEHSe(Gcz$WG12o#$SZtPXc9^& zITU~*NB|{=0+2W)zD=%3x_e_bJxO3rf)yo4>Ay8dQ5zOvF8yp4NCIZT#>&LXA@MDu zP#mQf`$~e9l?_TTu|oYnNUsZkn2?y5kf@M@gBdvsSQ2K|AM1x1K>a8OB5^0m2bG(R z8UFtyy*&_v>fs-e=r(H?Ndg-6LwgjBP;EP_k?4h;Er!s~(l1&xr{6#rIlM;g7_(gg#Ns<7O=nnG{f}1#V{)YO$*KTD6BqhZpB_+jY)9b)uLvJjC z`UOz>HQ)YkgXs~$^j_}n?l8G4%msOzl>WOMT;|eC&f-ZVddR{0ggpM^^za@eNhy@U z)~#C*8IjOJRd6=_HwM9bmgHc1k_0OF9JnZhAL(DS4eFPZniG&j0%Ei75EB#p5k9cJ zK>boE4-$!e&SA01_xP-X>F4yrOvn->e~W1^`wkpvJphhU&>;8^iChlo5-|mn@6)@W z^q**j$(LkmY6|s#1<#sKNoNmW5>wMD)Zm9%KbJU@;TWKa!9rlhNR^wEXSwvwZrnD@}l)E5&n~L zAmakBv7zfwK$jGvo4n9DqVGerP8EfWgZx|T)Om|G0ba~DxK{NALJGk^N+%MC!~2&6 zHEsZ5om?Picmm#+bO~S#Z@{%@KIuLq*%kMpX1Er%dQRit}>(7MmqwU4xx z^N4H*fw7%n$C24}TTDVFIC!QF96MJB@TWh3_=~SVeEJ)3`1~+9b(08EZ+3xO`ENkV zol%gIO9Zz|iJ<85drHi3CK5kP*O90j%SAoI9a|TI;t<@ zp$%Wf!jrG!+4+7Wo}CmgE&W-`*Ydv^&x<5~AA2Qug2G{~f;H*!LUp zi@x7hr&7heRnSU!k zayk@#eSbRAjq*Tw%@LZXpmrz<`8Ve&3+;e}qBD@kc>*Ove|T^8DIla(K$?$8FE0aP z`j3G0@<%{zZN;Z~iSg!YxKI5Ito3XHTm0(4mcZ6e^ON9Xt-uBDF{Am(o|qxh-g0!( zE7JaO%oQRitseYm?-%;zANmJ@|0V*ktC-E66YXkpl1*VpXNXY$0RZLP7*E5@%mN<| zp_?OG3Q`L4DIFa;X9n<`$VR$II_Lc6oEe=HiSrrf(v!}4IEVstbX2^ENa>k`zd9!p zg<0p~g-EJNAJi!MXL}gXsUWl$-Mq=gh0b{SNd=fkK1Z=6;zRL5q(g$p?6WFGU-rgx z$-WEbdSH|mJ>P<4516kqW?>XyglSopnHX4-yK{P4c*Vvu9c@-Hc$lpBf{zE5Af_mD zPv46Ae2IiZ(YBpl4YZv*N)Kyy4n5f$ z-Jdaieq&3jsS&Bsh3dFU1Wn?rBhu zukQ`@v&oEPC)QdNlbL$_VKo-C$=)#2tnjq?33ZQcShg+} zJ9!9b<;iXm02JEep-a~(9IME5UP~*+9Y+zVN)6tw+da{!A?Cyt>buEruj*x2u!4I* zSXZuApsd&EnD9PD+7MWfDa_spY~btAxQwKPr*|&<`AViJ!Dokjmy5L3>t4S%8sVrL zsz_U4K`W{gbIo3BJV`+D)Gg)ETO3@qw;ntK*GFDU1-d%8w{1D^HZkOWjyXgU=05M{ z+e2wWPfyokYv^-D%LnZuP72^Txf{+_^aS7b@px^#B{R5vCuU3gK}ArTymRB;m-h~@ z%BZ1Nmw8wEh|`Cv=vdmwP>0P-;>8{P_N>Rn8&a5YqoSAh&fdG{wlm8WEO>n%2B_VQo9{W;(pdHrET6XM^m?l5%I4Wkcm z{CM;Ac@C`&67+4!r7ZZRX3kg?G2r?`IfN=p`Nn>+mRDE+H zGfwB-M=L=`5W2MK?%;FZD5=0@Lm77sju}un;H1{FIo`5&nRqvrWHdjOrW{z&`c9f6 z%<2X*QaiK?B^m)Agf?KHeen4C%qvHHsW&`lN#AsPK5xR!9J$yHYuH|8@+#?7ZFk?k zPT8X={E$VBaZ1A06kQvW7$2<0Xkeww^f`(Blj85hC>_pmUzyx*yjW;-KzA<o`q>xw`ILo-mRP(r58Kd@-oKmc>a#s%@S5J(v&g#TnpaBjorCm$;OkEu{!?w zM(QFxskH<11!e*!@8|~bGaRJlba_E()C+6b@oR6NU-p|9oUc#6En9h;oxse;ht}dt z1E~%W*u|FGX$_QYVRP9rvu5bA<{mR`V;~EwxBI<%ul^@ZBCSyZN1#2T5w3SV`X4`g zc}5j8B(s-Z%!|0G_#}(1J+VI4qH~oMKajbwrS`3Lma6Iu55wuwEA(e;J4>Ucdmq{I z2JM??6gf|y z<-5OmexqL7e2ISes4#_d)4W8#RU7q}cTFA?<bv==S`UVSfr+{_1o-nfU*PiyB8p4K5tT1^3rHjw$9JMM3~sa7ENrQIp3z2@OoNNaz&iN;Fb4J8Pg_SaD^1_r}R&3xjkJI z+dsXM_p-y89l30HP;=x|!$(Z&?Zpq+A~Wgjp2RVRt$5-d6yS+{3xgxc*?oiCM z2#p6L-nF6ZN*6b-iSY19z$tc5OJUqoJGL`XDM7f2O|UAVqn#&2)v&1}_G8e;NtRCSSfTT3b2 z9@LqI4A}>J@aWwx%aSTO$LGf0lxl7!?3Wox1hgi|NA`w zF(+o!MDE|LJ*UJYS0113mW@qz5l~#Y(s{ef(j6NGm1G$=dS z9YIGSbf~z-0b^9;>1MCBqgxA@QV7{>;Fs(;;~lk*P)w=fZ)HJ95%<#wzrnRA`BE-} zfY=Id+`-zVyH2gI)=L)0WBAIu-CXW<&pf1e3BtdX<;|aY`joM-IKJhL@swQ{+g>hR zxk3hO!iTPFP5BH-nI0oy416zIXBUAMkv9 z-+Un7-|uL}Gtd3W3fB0>q&|t;b=T4taTzP?;)|EiP*s;IC3AmlV<}onSiJN~++bkX z)I&dg9vwL$3G#{2tl1^Ci_B>BX^OA&HRE+1T4{LWv;l+iW!=AjGE)5mr zC{D*)bXX4U*)p%-(9Y}UE;tOBOC{OJ_8JqyRV_vC`_foT=3(41SDRXNV{;=bdYsBf zcYBNPVCJ#x7FuG2Go%T~2RM5_D~)k9ZO5QkLZNw$mpL4@P;k$w1E&*n>Kv-W$tj zdSH21<_uV^3vgIVNZm88b<_8tklaNI!v*I3bk{%9VNwhwx2_%Nvp;(TKS8O46L0nj zTh2#+re*uyb=9%~OUKQ_u$`kc#XT>pj#VrfW8pS)OjJ(|pc9?kc^M-rutD%?J+^Qp zsT9^l+#$v0c$oogGn)TuHzrPW}t~jc{TGBQI zXi#6rYYQP5K5I8fcHZBnr_q>&F; zwp|zBDquO08hr1~lY=6QwyOZe#m_PaO9bk+Y_{QVTG%xjUc5i&s=jhppIF#rsHa;; z!JBIPE5yMj-QqG>c z?#8y15c;M_BA}x`YaWaGhP^x*;M8us%NY!#xq;bgjXjUub%{dtLVP1r5U4s;_ajbt=M+@nxL1P=Hiw zMN$cf(dBz}j!T{=tl%}9oY2^Bv=?*hR{5~4^cK}gj)F7w*;xJi7eQ*j?y9LSlcL^B zR#r!*jg*IZ=_fIV^O=v2!I{uFS zu;8hT_BW7#^AiL4Yl$ z3!pC?EL|DAIdi}+dUUJj=!d43sP)%gkFOd%{m@-U#I^0Zx`z1!j;NS@SyPivRT29! zxV=ka1nP#R1F&{U8|SAC&VN|F*RNGVJ&E6u^3@ZwkzWES6-0kS6NPRIfyg8+qq`cEZG6v zjJaNydM4!Z@g6$ypmUvG2iT>#gSS5OaV&onHmTyJo&F|aOZ$D|Y569uM%BK0!z8B*$w^S1u^yhD6k_p^DsaV{jV!}|E zvu2U$jUAL}zT2~xa%c*yo{%)J@t(K1Q0k2$^Sbpp12$E=3|(E$1PZa*?WT$zf4oHP zRYuaeoprk&$^{h%P8wPyT-6`r>d(tw8OM&<&Z1{BS@&?bntGV)h_~ZIT2a<#K0EMP zjb$B?3w294OoJk_8fo;~WvY3DB=2{PpV;o~yKL)asj)U@i}4^9`K;WI6(-G#Wcg@E zWTrgao?WA1jA-vs+glrDa_4qq++`cWtIX6KLzQOUb&`y^nYIjt-jCi)2ys6JWthTy z3fnrz@9e$S7u+ANw)t^&t-K54t+hsk>ltyZWAm9jPG>%JU(-QnyMV4>_lvY65f%9q z8*I`B>ZIkCu$H|SnWBK<{sFDFHT8DG0Sv_c+l`*WJa>woD_j#XYJAKTg`bL|f-$EK z8SU+>cVI0#x_u`qQMA<#h57V~)jkYkhQ0+`HW_Yqd3>R#C{+YE<5m(G9_d(#?RlTaw9od9ZT{2ScO54>8i>tI3LS^d);3>c5SixAlFbu@Rn2xoSH{{~Wj-?L z59|k&QddhRvr{-@>{(MQ^kP}ZWS$IP;q5maiL=1HD-%RIgsi zS+k|*tVM!WTY6P~TnZ~(Hnhu$5M%nx4(wR`RF-{PqPu@P9&69r(X{6*+aA}4@34W) znn?k-Q%cMTZ_*w_F=|wgaevS}Zq>a&>TKZa+v~6QQZqm3WeR88wSNEh@+N)u!uFzm z);l&qu-un1oX)3qsXfayziO@0%oecDtevj7bow5zP37Ur~)iUJWZ_1YQGxmB5 zTqVfawcUGMpuPD}R>wx`HP6pJeUCdOYiLPm-TmZw+K9cX(uI+I4nZr{NpZ>xg3RJE zfqqSn%Y$9T)1C}Nn~4#78pe%@mvED{Wu}bwHLOcF)jzMa@$%JOICD2eAwh#vML*<8 zC2r~>2)EyviB-FPb}-!c@TDjHJ&7YNN-f)*Yp;&WLtS^46Gn}V>eo>@C{jw974t4G zE$ZCJD(jrUb_Ba2cXBREr3FpR9jfII0XHwA}?O`fE zlfBGu3S_L)^)1+;kD=tB2V09w;FSxVIxE#~Pat z0grJjjxw_fYaWaO%hx3_t}t|mi7E|?-}8EC9u7R^u(3-#pm2{bM?u;a%rTq}8>N@A zC=GQX^H#eN{(ib?8BHFs^IIo9>@NqT?v0k8VrXkp;9i!JDz=B3T0i831^!*n_?_-i zDFMm(Z>|_N=7$zg7235`1eN#JYYT|=u|J{07=p))V=Zy-%?Kc7yGy6=gp?UE;?Bi) zQ;bRDJ$Db8Rv${6huJQyLL+ZPA)!GyOTU4z<}Lm10GZabb;*5iIs;WN1(&?M96q_6 zz<44eE{NJ70UxbOK_?xwzN12@Ayt^o&VDU{mP>ch-799%1*-LiXM4_Mr(57B&Z+S~ zQWP|wYQn!5nNaBp%~J`Cm<)}QTM}K=pzcK2{?MUpEwi`lQQ7%!IhkSh**VNqr4r3) ziaU3?ALvWDvj4V}$D2Do<8GB1PBRz_=_xJy1BDqKlfd~tpcW1AQXqB&oqPqW*)1Wz zKF-@u)QWu$X(SO+hi`P9-qdIqKFP>NZ$z>ASP$KTfT1C2j-bnTSoYiHJ@9$M2K?>T zADGFmW6Z*I$Xt%DSb^JjnsKZzx=Xj&>`vCE_h*-QJaRlF6QnoTooZ@xB(pfskmvf8 zO^$=K2TwTmMxN7F?F%a@mhr+Of5%h zqS6~i5+1$@?j@GX+>5Nb;p$O5@bX1epwxW+DA81o3nt@*85B}_$sCj{IH$5zF|Yii zK*SO;!=%lVRJ#wV-H)O)X}(w?t8U$=SZN<3tm8 z>kQGWw0LnDZ58Gg*so>JU@tap<&Zql5fo*UfXd<{vLQ53Q=)&+Yd%f7U5@<%`^=Q( zYAa~6Z0T(YyYfvY+cq4kk`!LrBO<&sD^qpQ1FGPd=X)@p-G+8?HC4J^qR8qFr%AtB zV%>(3o$EH9t(pvYdn(AHXFN1NFl(s=mbv>wuf?hi(`8vD!{FZDKoMV^TYRq#@4jt) z*zGjHz}mO_-nsej_h~-Se9YK3&i8_0EdTZzi$eNDI`e!F-NU_^g>P;cu8h%5Wb4Q~ z5w$)s^MSqfYfK+a44qH+sUC_43#-`(U5j58wVeL80 zT@>cXEqYf=xBYri=3xH&jW4w9@0At~9-rzxSj?Sl+3)U<$Uq--%kfHI%0_kTbJUor znv&@a<-@01jt}<6XMcP@sri5d7v~#b9W#i2nvXdG z8pbHNV{(@lGmKRhvwh&amaUa^#f+gaqcOg+ko6Y9G@fD$&|@BR472y+8dRRQqh;{BUkj zdz*!kU~9%P^AA(Z!v4MQTUvS@5^0|pfD=sOw7x+Ld0m+A1<4KvOfJH3KJiF&v%hgZn+~q- z#whq6h*rn*xGzliXv@7PZZX2_WPR=|v56laSrUkCt-&fVj9poTB@lN$^1f5N_tH5*y)~n&m62=I@D6;EiD-1 z>>1aezojy355^c!QnBaMn`I4N^FLH?AQv$7Zu{#E2Xn4Fsbw>69*WPvTcvymDC(-; zyE(3BN#6dXoz3 z&=Y@TVA%!5Jp!hC5_@Di>gh^+?{-!xH`SeY1wz{>1u7`I$2n84YCcjf@lP=KE#M3a ztksUQrE#F{K7*HyUn*UrRhXl`q2o+%lyxFD9s99S6}n6Pw}pX)fihCWdWU&1dI65c4F#cVM;RvMu?JHC`jOmX^u$5;MlznS}MA) z&lQMIdb=)rz4PuSdi%zZtxuIt+D3a4s^QDj^c@tyuE)cuKTeFdzAL2PHO`r*8MkJU z=z*&P{Ou*d6>On=am=`@9|Ej9U#%u&?WAASoVg*msZXS2n4Zlgj z?iaN0gF$l6>+y`T!GY0U_3CUsNk=Nq$Nu{oc`#u_*O>u!iZH1F>|_Kcpttpv@(|Nl zAbp#+YA^3OSKNoa`Rp@-vakhKtvygxnJ6z6t z&K%f=G4`>^`mpo-Slb(Ldvpe0g2BCkQ;EiR^ljS?88qhXpM2#PM~JP+o7OL+lV|QF z6h0glT*rdbem!ov^LoGV)*Smj*s%xd(w2bVmQ31 zuh+wl2isxVmzK`2GQz4u*;|v;IUg_kQI}2FGDidsW>Z-~AErdZGATm{G0;g_*4a9T zLn+utX9OQz07K=&H2#yW`}T}Abd6`_(Zu#Jjor9p!f?-f?-ifHqwzkY0}*ScEL%m| z(=dmAYL1LF_r3 zf=-oNV`ExVPVG7z>6d*A_O6^No-T)5Gn+?;1K!Y-r38o^FYwG>FpZAiJNssG>ZZvtr>;#U9JteIq5HW8iwPd zPx8PTj@-NBG+5i_)|$`XJ!n5sYwr#3F&Y@}tJtJK$wS{J?mS7b+CZ?{&41?1JHzBx zYu1`o&M&@Wr`e%dXYsO8g?5+LMaMNROQ)iCnyHGPS*~AiB1VZZqPXD99QUr%&|W3G zBrp_B`$a{?ohr5zHD-MKN-TKu|a|0KOdmKfuS{M=#h2A?V4cO#v(4dE#qYW$dw z43Df_dLQpZo11d4cFile8qb|wetwYV___=8ZV%FwDIMp@3x2j(Uv#g&yFsurz!RiPmByrR?~UZ#Y8QJ_i0I@djVAN zzU-A;K#KwOM(fYLv)_q|J=Y$HFKMh^-!Wn91ptats^2`7pI&Lvnq$merr9Sn0Z1xg+ z=kEaT_3s1b^P*FmP9{;bYtqC7NUVF2jIlB0rnzEdPKXg06Yv}9v+e!B-KKqA*=w0q z)HRjvfR?kdT$Ik3$U94)CHG!%28`m>-1+%#^e^Kmmk3D}J3yv1I0DqBcgCdtsaKnS79n{w>lvP&9uHJPrrYZ74 z)Zp1<*>wrr53ceM(+;JqQXp3N>aH-t$p}#B5GBoNMO|;keM~5Pb*=hdmVYu=S51;K z2r&Y?OevTf*c`Q93ogrJE-r4y%tXZhL4f%K?@rZggS};g}Ij@V{7qPZ?CUj23r2QZ}`Q zx&nbc-TiK7u>SpF!Gr*wmVp7I=IRR_G0e5MF5wnDaasCo&#km(`3!|UMg}`sggPFJ zJ$QA_G{O7A``V~M`z_SftOLiHDV+GqkM7GIvkxbB+`JP^czi@Vw_gy@I#i8CapRS# zL-_FGIW_vfuVt8`u}b>)$i8!wWTHXdOjGOay!vb49NBWV59 z)gC9WfRbK83;aT!^bb8(B`1tL1q(mE>h?Dmez(MLvd?n{_kitrsju?OM-eln>G)bP z=X<9wW~r>mnMi-k)+?}VMOt#|SQ5j7#(N0^^v~A~E!k8!s@*ect3d?HGa}sUmrqvN z&(t~D^$Ir}xS1`o&F;Ovu8hDiFUJ@?xL(Bq7)$X3+!Ek+WuU%oaKqE#psiG{T>{l*TbEpXh5AaU}y{NC> zr;~5aL<{88DDiu&@O?5hi48CF-Jq|cxfVs=&3z}w!Z&ly_QFV+7Ps>Vm9v4oID8AN ze*u@d3_sguMa;!cY09nxa@syxgGOlzjb-eGO<_s6DBLwZK(%1%@i|$-vBCS!ulBp| z)=`!|pDm=BF}j1}jxzVU`)A@C&GQ42bTwrkv2lG63ziDS)IBk#y2o#BbYEA-d!%(& zi|Lu9(44Zis!2LwisvuYw@#)VX3%qigPY=$yB9O7G}@nx9dV!Du2jiFTi1K0?*98r zrki3mu^$cs@^*>IaRTfMIPvO%?avrgSk3c^$8QA~56q9h);yva=QJKwbb4~Q@FNw5 z3gEPq9tnidy5$ZPa9s=xBzCX(5J7k9Yzw=Sl&qrP@TtVWTiLvOgN&xGj#(!J-?tFz z&!g#P)hz*hrJ`|Sx*T`>grj<&@$ooB;T05Pw$&*U9jZop$K0#V3a|8%G2WfB+ybXy z&~8}a!+3%@l!K!viO~1*yvH_EVoj__u;=tO%>L;JLSx?4`-o}1b0?}7oSD4!7O$Xn za+@6sI4wLec(`|aC4EDpXn_dWHYCurzT?aZF_{@X$K_MUZ=?plF*{6Px#6^Fe*c!> z9<0?6)t$h_jWfVI2qe*7IJqZESL1C>@1pGICpa8B%uc4SuBm0m$?ldMqLLoT3`-%n z@Ao|+qS?5=da}_tK!&OvG>JGEPzD$~oc33q+1+}s-ptJ{(kuyIb_@5u^^C#xg4{Bx zdp%{uz6S&6FYPf?cwaalg5iB4>6u)?xq~7mrQ^GMxnHy31aO^w14$D{Bv%QnS#qAr zTk>GAUd)OOqRq4mXOh63HcmgswXlA-^(WJ#-}T zW?*#_-R-Tdr&KP#rMhdR7{1=)&hS$>PDtSSh6xp31g2ri$gXW7n-y7~Xf6!0-zwEN zIej8=W5&#+W^m}5De-EN@|8L_B_4wu+q8h)D<~WcjR#J+CKd#|C)CC6(NkQs%V1Qz zD)Z_2T$DuF>}cwM+dPv1=?AnYy&)@9?pi?yJut&Zil=CEVK2T6lSsaLDB4 zxMSw7Y#oY^7F$@B7^_B2wlX~H-@dX}dAE$h0{hL`56^ZEH*M`TS9_*56d<>`}gmNZEXGZ8U@<1r-icz?WPp{J)hZH zq`XNUIk#TZWpv_w{CN*2SizDO+mC;&+`;5C%AeSFvbx$o$f8Pwf0=f5iV0qXK|Upf zJ76g#PCQ4YTr?tFM;Opx|I zd+?gli3Afr>Y_HiWw|>`Z_zS;0B;6b`+@?kHrV^WZs_LPt*7Xw?z_g`Bbev4Hb(2jGidG94f@IT#}X;| zA~SqXahb1R@GjJet1k3_#+L~0-nrtn3w|Zvz(~in$M|98Soh zx9V|0!(*NIYQeG(*`C*r7Wps>jIl;uWE4CKg7bEDnwjai&RA?YU46x`m*)Ai$A^x= z-gaj4mRVsgcU_s+Eiql98g?c`_y)t|`gaQ{9d7ipzpmgbEq){Wgeti($f^GHIh&dA z1!YRrt~GaVC?XEQ!t0XmYN3|hs}9DTxEw@jm=0^YecP>p`lP^#N=J6xltY!{hMkjB z3+`=^4A|#)7@Un!UvH!oLwuo`x7Piw$V}V$(ySG*hSEi}X&E-`?ici(sk6>*9jScL zw6-W$v#-1*J+ya%bL1C0R{V4K0LCb0RUYFn9FDwIdmQ!fzs8#NHyVKjJ=i zDE~(P`&;__ct!7oeXexp5;$r$Ini%3Q}^lSbEJP2T6^D`JBY10+~U z3jYvQIf%S&%Fs|ip`++9NwaQWjM07e!)qf%H^2#2=0NLM??!9)>Elsls&?<=>}n-M z>U)Bd%ti*IPxI;~Gvk@3sI^oUwnyPH3u`v%zQv5*klL~+*sV0k zNvN)M@9Be#(p%)?2LimR+mnKCcH~PY!HED>-6?-xxZDwVRl)eNQ>5MES+S})EsizK z89$$K>jJ7e-1R~`*D0qS&Nsj^yMUO?CZe6y_26;kDK5*??_^@c&ZJE@(M2Q$@9`me z)fbNRFJbrJsbb&ru-e}*h&4DxFG$!C+!H%UOGuLBN~KS6F3JcNqrnw+HSAjz(5B|d zY`~Wi>}XB&vbP`Y_hR>V%`C2Ne16oVLl8sNK3_-R`XPG42u{heeJk_wh}S2!u$>+* z*d-^q!g+c1<$9$}usqGV?ps+F_}M+W*WMRay?=5l#xD8_l}Yj-OB}`E5saLJzJDwI zU}?wz=5;T}9*ylxtYs(OxyIdzA$ZT z&ZYQhcY5^sJ%U&0H0+xU`~&AX0_n1hV4Q@*jJ7-dI;wW=O>wQ&y1R=Ht3!3CpPxOY zb0cCw*nOLVTV8FCPDh7yL{<06GT6P&@1w?kAWY>1ts1;+ib}@L95#vl@G2lpq-p7+ zYRv5=)B#}*2P9ypsPt|NUG%9Tw!W3mKDt+}5IJ5anUy;FcGm&!o4zmGG>(pcfl~+lOmQ>~h4-WIvRz5>DdHaaJnIa1u#hg4#(~_!;x@Aj&+)8OJdP z(KJU3qTV_-fz4?T58B-;GHk7PTVL0C=rBHg-}Q`ecX(qnp@E-DN1DBh;C+j+?}4mF zqOG8TBjLl%!O%PY)%zRWx(xFtt|lA4ia#LTpz>T9-hoS3obSWf($_D~ZAk513PQ&M zHX8I2#_X;Sr;Fcj^xdoKj(sP>UimiLbRog}xi*v#(F{50xz{4DeJg$Q;gHyxz6_eY zQE$Ch^?AO5dAEyJ5ybDWjts|Y6ofA2$G`D7$heg`w8i^WfJi8D)j&eb(9|m7HkS|4 z&nn!u;=0C!;lj%AeX&=b4pp{raP}sH^;<+YbnoIUtd~z3d*!`3o?+nln}zUBY`aGS zR4~)qRi?Tj@zJ<6o3>>HZ(5wun^o%hoCU`4hBvHtYs2E~>?N#jiMq?FL?=04R2Nzk zps!xum$HsmV4c!vc;B;IcAn~xAkmOE#6C-=TEzJF)pU3p^>(f_o0iJ+6@F8qniSJh z*_sjJxHt+E+LU3;7KyX1nlBbEuv9H+PhD(if}x7BIa_t@YEAxTNgK(fGB=DP6~XAT zJJordxu0**H>f63u`KqZW!71A1wK;%mKm-x$k1~LMGty8KrVtzL3-Q^9{fZ9An@lA zn0wp>P(oFR;bSi(riLMz$=6UwPDhr5m;RwYfB?dcj5GW>Fn}H*qKAwgE@FlZAHO2K zngczT^(Dif79+(m!4RcIGDsHbAo~UB{)fIsfQ&b)%Lp%o9l{blBF_UEJwAq>5`&L< z0WrwpkR>3)pH2gd<`^BLYbZ_>vM^-yoEdr!AL&54kl*0Xr;+mb#~-s#$RQlan4{OX zAgmB>qL2}u=pk=u$g+^(7CFE|mWQnH14h>oqckWjl0mXa#{$U6f9SDr_|tVH{~|x5 zypav44*dT@sH~{2qy7QabA$)NTnsX*&oYn^eh5qSt~n*hDv;G7YeLq6tP2_U4Wnx) z4yDn6jAW23(t&izK}P;SZABC^YFAv4QJ)4Mqx?Vb2ZTGSYpARUck~($R3{N02p5Do zs@DiJHAc?G%izmxJr*rZPlhk0&4oWUR*N;S9)T|eBL0d|8k82vK-qIbSYLOf3+hDv zL%LBO$QM$OQ5%~N8MQm)hyRQ1;Y-|6AB@V1`e)QHpgscCO@xbvki4H8dSESF$T2ljrlo!gAPs(`{v;p-~$QI-W)E-b@h@OH+;{)m^|BgOD zHlw}<%{R!nqrUwU?(oMG!{JM-YyK7d{z@5H)8ZQVGPDS2n;PVA{lE^dq5ksk)E2(1 z^YftXXzqdP78(aN;0wwl7n@bQ`71H}D|8{7#MNUDAR8egKcF#53^F(5ze`&{^#t{u zXnaL;J=E8raa9Gre9s-ep#3lJ*Z&Iaztn-o2_dW>8Y57@gvM!9f6zJtja{fO{a@{m z5ca68azIA?Jlc|yXX27Kk~YrH`Lc|Ehhtg&;*61CkWupaG%KRgS^)u(XI_*d|+=bnGHU(q-{*A|w*@}T;J>J_SA$Or!`?SNeG zQ9p_LOEku+VDvMtz#sS}{&}v2I%QDku27{a*nHDSWWUJ$q)YpAHW})u4dv=)Q4q6 z^-B~oxgGqs`XX|@M}3YsWGu)0)sz;C?LoWZqAIJ}L z?SbTn|6DsjV>73SxfLucnp2S5!GEJKLVX{~7u5mO-^=kVw6m8sxjPJBAWZyoTma<@ zi13HdiL$0eM7TyE%B^?+{e%4RnAA3A+rodx2WU({YeraR)DF;ifX3wiWM71^NBNxa-h3;BRc)XD;4un00*A234xcaH@~KiX43>)M6%tPAu+ zRim5W`knY!G2E9R!WWN1K0}nyP9(w~>4JVuhknQ)wF%TWeewYspWz&9)(8Jt{E-jj zRy}HB=2pT%EYKPa%|X!m2lWsC4kGvYXuL)FEt#(vQZ1@_nE2Oo6j4kqhA5^UOGN%a zetK^4S80kRlmv|se^>W9ev4jT8d{L-#hgcU=G ze?5m1g_rCj3M+*ZMU)Rf-$oO~HIC2v0Ja0v7oqWBwhffSc`2NmqWRE&crNtiS`TA* z^D@K)&56)h@bBn{P~Asu3+>%wSoyTK^Goj_{`Kq%bwZZe3E7<}AnQpKT)da$54dkf z>Wk2LfW~ArFCxu{NPWKf1_K=B)p&vfuPxi%ke)z%%$RE%TBp-b1hcA4X@5;D>%F_@+Jl zaX)%_K_bB+ZTiXdVRD%%pkf{|Nrb2f|Cjwn6OCx(LnDziR^! zf8r@JC(VrwpYextAc#L{pYDIC{-ZV^p?x+4VvpvD-?afjh(F?g z%@2RRPx_;N_;!5$JK&EstL%o@qj3Pug}&MwBIEzr2j8@Z-`OX9Jy-hAjSYVX{81Z# z>-Ip1J=&u|`vqwJP9j0=5>oyD(|-8VeG;0pe%UAezOmtJ{2%^L_CE-Jxc^=W0Z01; ztdKvA!zBDkKKPj*z8Ig;`0(!<8~(-l>fZ%_nYI2Z zKm2>g2J+hN?}Wd!NzMfbI$Do?>Vt%I9uQ~w3m^PdeG-~4!S(a6$A&+$hW@@!`a9u| z*2EC|g^5b0N7Mki)t85qW%=ohKE;AB*{(R`00!oonbnI$!})jREr^{)oT!!?$hW`#$M=^oc}-KY4ukeR~<-`{9r7W&GXnhkFv| zA@=C|60|Q3=IcHr;s41GpZX)z9)9kJAL8#?3-SLvMw5K-Z`#Ws&z1ge_``b#8X@dx z3?S_X3F9FCh=1h=a$iIaVW0FPT8;4kw1)nZe)ygHtA98AQ6I!1v0*0I6K12ov81MuX@5FG+5hEH+V$6SMJWyg3#Uq=n2dLRyBPJSe&AN<= zBBC6MCq_mPZmkG@8S(!S@Rxmr@x38eo8%v_wnqH1KHv-B&pEOE$|gK#35i+{j!iP* zA9QZ#2VYswZEUu#z5QKl(#)4qVv#ArgPNPg4(3a87s4lBI7N#;a-iXze?9E60C77k}1+ z#9r>*I`H=m>9CFZ`|OKf&2sL|{@sv zyv0JG`JxqI&-wWMD(YOk07!QU_7%!2~4n5BrwEIxw|^t#5tl;-67H;60%gOuAi&fbLoY3Bof{T*8SrgYDvADM2o-y5=+duzq`^ZP?dZTxOMgW zrd#bfP>H|X8{eV5-&b2B{tJG!_gUc2@6T=D!5~tOLCs(%2*uh{QkaTgIg~y<@t~dUe~E_8h3; zJ9p}PlG!G;M*Qbp*}M$+i~08+Q`XzqD|O&$4*~UH0`jt(w_B_ODYQqFAOCWjwQlq0 zrdz~0zBkDEPcavrK|Qoa{J(d}wqt?6SOeGNnDv$rLp`|tg25&Qf|=nDv`77pU+;Tw zkl(r8^)a$fkb9?ZXpQ*So^dHZ05$hpCn2ne7U0}+*&Ea!zY1YTIHvI8! z{tAG1*T3~V*3p024ovj`{nu@h5zq`yzWVl;)-k|d&N=wr8$#yZ)O8uH4gYg~`Cm5! zf7dUV_)F2-LRjCB0o3lLJ3riOt$zGtYyLmJVWsAM!oPEeuFJssm|@fIOMU;DU;N*H zW?lOJWZ>_vf3734pj6D0pBt>RvmuQL)O@{U$Vx} zc?0-2Sqpykm9?lyzdz5s^!;JPT-_w@o_z$H6xKHwzbVCGlMMAR;^arIu|L=h{NJ)B zp0fk^?*;x}7v1-NKjI$^6#+yTdmr;Zv+3G z)`DyPT9o*sUCvSUULH&QMI8jxgX|OJKHI(_E}P_jV@1C_`t(M}_H4H%E!br(yykx_ zd|xao{a>`ar3rZZbf0z5>jdaO#c2mQ)k9wT25(z9>dfaH-QQtNTKK-0dldK=(cB}x zx6LKirmfcr8*Kbd^&st#x{s}I2>-t7&PNB1mF#+8-%Gns?CTbNScJO&^%mgn`rGMs z(EEf9+aaif-0C4)n-pp8O@D82zHwJhdd}{5VRwjq-QtgJyHiAS?{k)ZHc7`^{lE8# z8>T@l)`71c;;>2S-WvkGuX6q~-5ltMGqwtwFR`y%{7F&SeSGV=6Ii?Nx+?DKu~P@z zv#y8OY*MuM29AA4p7jQ7PD<~I{p25hT1@Lrn!oq+x46bL@on7f*J>x6^RAVI(6)PI z!F#T)uUqsHY>tgPv7h|I&x!{B`OChTkT~$E2b4@)@ZJpq4`Py(EGFI`JMxJS`(nW# zc8C5IY3)u5zF3ah0{5qEJ)|z?Lp~EaPu!&xdd(ZgzE1&7<5)iyltU6Z`ui_-oV@sx zEfR}D>fFnErbug$_eDI_za7Ph7CGeDdN?^V%Qe6MuYH+dS{8=Ay7U5_5fDGxHVn z$)7gnt3SvW{=mL*)@2_Qw6E2%4*9bpY)mjMr`jvuAu&#-@<6~{O!SN!??2L0hJ{bqwbUo@Jaz7Cr|CY@d!(R{F zzp1u83dWJI!5mBnw2SeuQ4RL^NxakfzVjLI-wj{vM%a~AkOtz7^$6dXaZF?Yle>`i zg0@;0o|lr`)Ayyk4}pCM>_cE50{alyhd`@Cz;(`&2hDBT$wQKq$zwXs$R8SUyr1&} z4&_B(i{l#c12301G>PN=#rXztTq2Iep?!0Ub6k%f>}1-ssloAH=X@!S?Ii6)o1F7Z zw9z?cjvAa}KEOE?4^WQfquR8ocOS)v$;a|oKDJGpQkf!BD0Ne@W4+3M8TVCgf>Xfx zM?F^8e?~dxxJ_NQa?RIgF%LOoXNPCZuZT0O2&09KFdmGkutsed$` zl)q3v8=b4Dw@uD5^;y+(sfeXGFBP-~$5M&wacnvF^3Z7jcKfmuwcDNA9@TbgCsZ%) z6*t?r+BvdswV&)dSP>J(-C*p8_7L)K9RleJ=>}npxFa3mG0*WgUPG=lely^` zOoM4jOmX;)+lU*>$8wUViSx{dLWoNRq$i{gq#vX|gna4)HQ_n_#%p*j@8P{ngJ}^@ z#-)^4eiL+)Wng=wj?kV{cgi3vJ7eGxlR=Onkl~O~kkJs@a$Syge&aQ~miO>proptB zW*LOf;IqC3;j>#KjuJO=Qc>5~_9`LN8|u|i$mouxQ%?nV{8fmnen-FKp5Bajdw(I9 zZJ6*HUdwwbM*sS%j-}IQAgvJ)e(Pr1dcJzWBEpv z4gb}}h)?_2fT)9N@e3&NMrV7%&1WhYO^vsq`vk$GXBm^bFJnA$Y+!oGp+g#BVU zg!TxyO+BUe~!*-0lMFSM1+6Z6JAGOx^Yp*Ld3`ur}0?S#Y6!QbgP z;Fv<Jd^4I|#2TnoW*siM}w8zYIA$3t=N83^gVLKVseeCTw51hW~OT7$*{vUbT z)1&5&8<0o#UF>6+XXd>S7)tCo?q!=8$hL|&6k7_7Q@ZE(JN}tXoyuqM6^*u;<42Yu zpBSpxu?`284*TWBi1WUudioYCYZ39`U(QRMc*eauV$lCvjJ#Jv${_i|P{*#iV$=;+ z4xF~p%3U@w#y!NT1A%AwSTLQgrTe|&sNXCFb{v0G50b`^*zw({BZPWCq-w&wk1)n` zcCyauf1)jj$!#G93Sz~+V8@GPeK2M@ZFDPjdN>@#5nfk z_mCkKNBz@dy(ZpgLL3S zeve>TS>`x4*>3rbqhHs)=PkoH0P&~ek~^&)<9=uL9Dlb}HQ^7y@geB+pP`FSi#CmZ z$+n4kz_4=6Z?8ZZIksV0`M$@p=ekjssnb2W^ql$A@=-Tf@yU%=#i*Msd9$NaOX zld9T#fZ>C}F7!WsE$rb2+ZN^lKkDHTgD+i(vhrIF`;%PXQK-Xw*XefP!NZR~V#sBC zjvTtwic?C5US(nY!#J)2#~x!>K@V43eI`F5Y@%q#=uhmvB-#F$_;GC0x$?~8P-ebQ z@w;0tI>`Q(aY_1h={aj-*FKA_c;$yk?_3Ku!8)w}j}edi7ecptjJr$NMfOeX+u4Tm z(HG0{4)lH_%FOp=zAF<0--h^=?YoX{T-lYe;iPD>fDUDRJ}HPFDq`slLwxCBh+j-m zVyt&ztotGUc+xVi_rZf6wJ4pdr95JF9w_ButMh<|txf|TvJM%z$|@iI8(|CCpRf(v z{RqZ0d1}MNvFEsdT@KL7AqPQxeF@^?t_+M#tl~JrALA`^93Qi69Bm9!I0Cr`RZ#r!ZuBRB#`gw}L)fj+&^J{?}-(B7DpojyZ#Ru}oq*2AdJxxpo@$rsBfOJx4||B zjdu>^SSMh-BgT!6Z7eqY(1v#bJH8W(am*1qhrSZyMsL$b{IpofV*DMWZ7eqY*oN_* z&i4_nt7+qntGL#V*W`(flCq7V_z}mb9$gum z3BMUXdfA$Bu9Ph}wuP~m-ETXY*hcMLuo&@U9|HWyf!o%N@c}+@Jw~H$8%_A>;~DjT z#_zRbxvTHgiY^+)J6+kgi2lX?Hc`a*bq0QRoOYFqUBTPzaLUs* zn(%Y`Y9%fn$2*f}Z!*R-rw+<7j~_<`L&r82F@Bs2FEPbQ2x`;U-LcbW-t<039XH;( zdYA8d_SF(UxeuE|C}R9LALqsyuc&k4egwsXLLYOz0Y5k9jS~Y(jCaI4^;*%cbpZJe zPJ8F;Wy`!*HSUP1HO~7T}vE` z6u-5=PkjFikJIhN(TJZMJ8I*`pqSjFPTSy%$?djh!?*8Z#1GuO%YdKw{>=D1p7@c5 zn6}b3t}y5y<7ev<6ia^Hrq7*t&f(w5)OYG)#Bc7>|2zlyiTMvTzLAcjZW~wJO$nRug@7P8ize2~*1XtO+08ZFG0w`nu`EWGEoXZ5i=h3dy#zf{gM~XK7 zwy~uC{Yk_jopZ$};{5|WYXyx31@6u}0h_!2fblH@G0ea=%J)D$HdfHPpmE%&V(82v zFplonvo~7AZ^707UGNx|;DS7vdM7tGKByM!%HO8h&p7QGeT;0=jEU=siyQG>P<$&3 z7&of6QN+PT9M(BYKNT^E3#orj+jZA&5JML~8OtyXM?;(T;^-ak=(<=g$2#0=;~iHY zmGL)mWE@*?IQ8jbhPKhGi%QHo!@HmdLscKuHjU$>&UglK zOt)F{fB9u9hHL@wgWd4-8y@`hII*%V%Y9gz7JZC)ykpzO4B9ks)aMWwgMES<|GRK) zHz)>CXlzS~pR`IyVU5G#S(fPo@ z7e_|NCF=h%;zyl3KXC+Nc)+;XuXjYOq*%r{ao`8sR$(51>&{x$HeufOin>J~-x*cc zOxNXtAN$s%-o^4*5hdg2ZmWboGTv;)aRsx$Z#!_i9&t&_^H|2bJqmryG`vG2UTL%# zjS=zNj_Xz-E>}_YsR@HxGHKxl<n+8q<9jmKef{xX%H<=#GycBYuX`a>VgD_Fg z2Vtw8r&oyF)MM16yP>Y#O+BthT{k(u@L1I!Rj-_5p>K}v2|aXlQs}2;r(+*Ch<|Eu zPR10nbz9YWk*L!a)HWit&uKeCTVnO4^j+ zZ_#el7OvT*P%9{02TGCu^EY0nY8L+eMs6%4%S|2&axsvThCbbVzvp~E{Xcd(QP0}nuLOtSvr*y#@ETqV9^zHJmuWa@kyDxa zM_puFWL*?1PQ-<_fgBrbKNS$x^-#v-K&+uVd4RCpQJ2`}n8!kFZ0qcwkA(C?9GS`BjsHUuWzQr{=HY-& z)pf`d`$P75%p>!fuT9z-_8n!Afv}tBGEPh;wzoEi0w*seIfIRe;>LmaWUt7?C5d{4cu+~kJfJ;dSs*ME`u~au6aB@+GCew#Il&wAmQHBp&nOh`|jDSvIGPJ{FhpY7s|?{Tll+j;Znyw+8XA z?g2LC5Pr|}=#a~W=hBRQKxxk=OT+2?4R3WX1Ks7sF@Oa?Fl6qMJkt8N}uAhO{Vxnj0sa{eDMAMXTXNv(nK3mIf`6eGVoFmK#7B+^0(fMJQ3|K@?<&QB58|I*4}8RT51p4q$ALn$#wYH)1M0N z`d*V2-t`P@Q?cPg{#KO>PUq>7Z7x%e`W+v9Wlfs-QpWFe#fXpKttJ>?JY6lUgBiH=N@nXpL*UaLQcV+{PH}fZ#$t! zne=nRr<2!l6L`&v ziU=R}U*L)N^RcNUfuSk2O@hc5W>f;d`BrFe7IhxL~z#$F3J#| zFtjxjKI+&qrAyCR>ozs}@o~oC>N}mSOJ3VlMELLYYyg zK9I6a#efgEbe94j@%_-{;Zr$Y4IGBqa)=8%j`tPahMRGE+8Qo+hYQ=kgRgWF+ZH#S z!!?L&JsW(8joF49cd8l;ZLsli}n1L&&`6iM&55Jm)*SG z8gu4LK2Fh~@jyzK*4p|5-eoa9ye9v=x8Stl9ELl;L;l7v-b>YfyM3Kzn@r=&pm!Bj zpG<9T_(>01V+B6o1ug(Susw6BH*!-h!T(Tj2vN^RmWgPCl)0w^xDBn@WoVCsZuZ;8Voi>?#=15HRJz}SH zOcJA{@F6y{FaIb}Y$R5-bKmX&{YXTQ62fP5zIhnY=d=e#ut&kRB%9w7z^4)89Qs!l zWP`1~J{uf@?ui$lMEa?Wajre*gJ~#lEZnd$;X_W?aTymR!5i>y(0=mbPjc3!T=5|X zXVQICE-;e&5p6DOyOcdXEscl=U!F_e^Lg*2g}Z>$XDfs3Owjo(@L9Fs>aSW2E_xk+ zLlphor!#@a{Xv+h=Of~CKe$F`g0H(^{gPhJxc&!LY_U4}pCML_vV-D?mI-Dn)QOk{QxHZh+gfbDq47g4z_n?!|G5 zbG`}3;6lY8hugHqK5pRoQu~;Pg4$H_nUYdc@|luqQu0xf+EgBs+SKK&tmknf{$Q(t zNZ!&?{g@YEammjq*m(K)(xs2m5=AY7RR>!P(7)j_#+fk>YJ@Jj!ri5ntJ;hsJU~g&DAUD zNgd5cV-P>`<{EkYwL?kDYb>$)&%TEL%4>|rBnuk{X@HcpDZ}4=Tah5Z=D~F&e+S`O zKdQD#xE6=af!C@{3jemcjb&hY=sU$`N!)xjH$#tVlqqShc zGP7QpcltiFoK+C=K2}49LWV;|K*(vS%1Gz8A=vX;aw78HQV99ExW<@R@EOInF;C>1 z=mcSYX^I9yh9BB{;Y{#6+|>7&C)Y;j{(N#RIBIS@wD+Q!NP}20t)3A2a<+%C4HW|p z)JwK$=85HIoeg1`$>TO)>V^;L-;|3m4e-_7duX4DuD61-rW`3yXZI8Yy`Z)|V*whzReK996z{m9$X|HS9F}z^- z8}PF$s%lq*$1nq#@CO5L;d9AhFh=M>Ir2-Jm}|el`#F9s2hUtXCAjl46Y?68oA4IF zb=9->Zg9#y2;F(o)`NsN!MY!P+oi}e`vuyoTsDq34{P78?)1{3S6W%guw@SS;0l35 z&+-2zxEcE&zZN<`-a*=ecx<7XPPc(Sn}NJ@e2}dz;C-F`;X7SNAKH7t2ifxso@X7_ z?*ecO{uKCKWub1+9_|x1VZc=K3W5VRW;;L}22OkF{qJ@iM}Bj*T~9jzmqInT6t?R; zf+pTK4}Xc^#tY*QwE5n29<$B^ZBD$-1Mans7`nnjEC93t@+!g>CdjJ@p7*;^c8<|J zeIj@hwj23wJRDOhKZ)ewQu(Dc90a$NiOY-{cM7gsa6=?oe(C{uKWC!+ zd_Qr=heqCr2)r5|T^L3V8=J?*!;2KiB?%lH9!c`Ff>*`Dm|2VmlF)x?3!r0fqU;_W z@cF2#e*7jJ!Z`j^zDSMVFNiC$`_Z@Ot^C9RHuqv+Ksz8dmS4(Y9-53{}5grA(o>zjSy`>~TZ%{<09|XB(&(b>k$Ie?OBirQRld zH&bmQh=Wb?LMr`bfy>_^ijS~fetW{%|Cc;# zRsw3 z*7>jp#(u)F4RQ=w;PM|7>KpIqg3eK;_L=s$VXZ|jICY~<^z{>-{_-ioGhE>Ee_QZ$ zrQrzhm0$6Dr!G*~Ce&>?%73_FEKQwI-?Ix`{=XMIB^ob~XcJ1i&^e}5en6=UI?trb zy~Xzm0}k^2vQXuBxVH8)a%-r(ZQuaZIiya%V5>zr?R;Bw`$#>YK5*=$kEQK4k$TTA zRQcyz`Q<~feo$Gv=RlP2mt(!}*@Y_qysJL{3CeHafqLH3 z+e9WDP~;cuf5Li~<52V!HFiHC^g-g_w24BMf7Ye%b3QIc^v^<8f=;?AO8nA-PeKAOFq1?4zM}>201N6@x&1(AtD< zBeTJQ7>qux(W;%h4SW{6Ecinu&U*sxqNh=Icm5}kLrCGHl6{2On{d$jOHZ3nxL{4T zVa!?2i?P4U{SqUefZx~3B0te=S@+!cI$?vCM@i!d3gS@;vJLv!DU?46oMq%BBbNy| zOyK{OX#K-?bqk8@ng0d1&2|sJjpW^mjDx?wjPg#w_bXn<_dal+2>upReZ_eWe(>Yf zy5}+YeX1b>0~-g8Gb(`NCvBTZ^G?wN?da%Pe{stWzrm#IeuZB4O#eUn`x`pKPBpQu z<|1|EGEz8ur2lA~b?r^7r713-?FsX@4`E%!BVO(ZZ=S8#^Z|a^Tq7IXhPs<2>nzTF z66Hs|&xx(zNj3_?6}e!LOZidm8|MG=ORmK)=mwiG zK3jgRY|76uExzT|7PQd*hnEPuu=tawGSP=j%8zn4IyTNTp7&>^uxIKPeLM)ZVHI#l zYab$%ziHmG|5*k5MxVG=w?QWcfluXp-~lcbKTn@t{`pt`Z8LaM?nhlOL0uQbt|bgI zb(g~L5&RxU;juiA!+qO61ok126avcpO*3f+ur;&+<%Z2qzCeSGUZzqyLLgK&Fp6#ETzL$NRzPGQ=|6e^H z@_#+w@_)UY^nvGA>f$5uvbiSeT|6be5^op(M%+#U6Iv6l(>LW?5c-+X$DcyKYU&Svqu-_MA92`fHs+b-V_Eo) zLcdx1t(QS6AUz;ej&#Sq98wDD2H`qo`YR9@mS46t92V6k>rs}Gd9Q@@fsnI+-{3g! zG!imO6Q1L59OLzeuvrq9t`OP^mYorab-#{qwoT zGE8_4ujM_w7im;O=(EbYZx5l)7qZ-ZHtUnV)IA~92Xq=dp~twpu8+gVpK0_M_q*#3 z=sb7=((VVL-c#S%-t(>JY+KZ|GD!bU19459ASL&6l)1g(h_uLNO!#&xQC6jyjjH(H%Z^N_M)@k!{@kzz)X*ZUYwu5%0 zPgn4U!0#N~6q0u!Y#-isGbuMYD!TSL_xs2L+ctgYscT7WtXK9c1K{7W6n-xBeN7`9 z@jJ!gB8ZWXpzhJ*Q@5BAcDUepy;u?s~lCq>J5M0hrxYhMV6X7;ZJ-QeLRIKdG zhCI{Gc*bGy0UU=l?7Lmw0F#f8?)T>SM|*t4gKHyPyN!O22A`fojpfdOBS4fJWghr|h4CSD`U%+Vbxs?O zx4f)la8Udf<>gq!tz&hKx?AqF{_tUb;PC$r@Bz0uB5Y0o+ArE`_EEOYiod+z+1LZl zrW&=5#kzche}eSWNa?xse^2?iN?*Q1$b+DAAsGCf4<4lQVmu=FAK;IiXn9%3;Mn*R z%F8}k)UiKaIQ%gJ{npl4U8SDWZ%_E{rF#kV_+Z<1SP#a}Pgwzaw*iuNt*Tj9OPS9w{-7!zHNqVm1m@oV?4wRioIP|v+4 zKWm-*!_C&oi*2c@d&ctXICR~u?`Um1pXD7e^~KjwRF0K|AG__xZ}vBtaqbq+H&B*4 zZQC~5afj!`sq1`}mvsz&!C@%xelo7Dx|TkrQvSE7XSJ?991Uu@Q*Arn<;A%5d6f46;U}QQaF#v-)N$RvkoE1_vvmD- z$k;UB(l7?8cXnTDE(J~%G*x$GdKB! zoVsMIr~L-~L%;j<<(6Z#|H#|;iuEc5_1n(3?qBhzX4{ThUg9AAsZ;IR_MJ7g?E=;B zcXdB<)qk4xO|M4_!d&J3)SYyxLBw|l5 zy!L-$Uqh>}$>IN~-|tq`8T`f-zbC!CtZ&`-OE0suXX0HXU~F2T@*Y56jY^g|pxpF_ zRX3Jjw`p@Pd0oB4nt?vd=OeC_yFlgrc6aF$jdF`JYvn%U$FF+I!t%0RtNr!`S8elL zbIOli3H60$`z}y<)B1tyey83ImavcE-!q5 z)}sCHNB^BLzs55z^w#gakAG~D2jDu$ZTG%!?fhV`r>yjOmSb{#Y^v&b0m^$9%Io@h z)9=VvZs_=#T6sN%)Ba(9G-d8)#mC#>!H~YqijQ}`%Pajobf0G3U+>I|Ueiit{`c{< zpIJwp@sh7?t7B93y;GF;2b=TOAI-k}<4V+To2meROLRgfh}YE z=9oqAw^e&4-bFND_7NvPYK>)i=Wn-??tfMPMd`B}?4N1!OAPM6WVuaaQ?=h7dCH%K z&!+I#zV>g4jyJHT{{q%6x2h=qm;r4&tlt*p*2bo4zdic&r{JIcn()s~(mxsTt(#B3 zXbZnVi>;URLyh2nDRrE7Es9^ZtZTLZl6pSoto6bl8$Km^sbv-AJ#oV{_!&E8c6`oHfUhyzbq;OYG&W^9 zD3c&0in$CNG@g5 z??wN!Dnc1DzQpl;ilxkYo^$v=k+Mo~;;QEy%IDfT*r!CxD$5*)FLbWuU2x4`XUqKL zV=w%E7k{!P+xDDwd6&0rKIiJM2IZrShG&$ZpScueZqBsanU}Yv8Ds8Ckq392Xvj}q zPGQVcK6&vctLhf9zlvtRGbrzF+F!i8m*=H089W5sJRM-qmYDoCbsW-{w`DVZIl;%l zIqRa|;ICSS@zhMa4~r1XetAnH>k(y~34hhHqAy_j46@x(IKJh&P706Zc^vNB_93tj zfpiEsUsoBzx)H&wv4>-0DfjSS+@JdOKcO#Opa2_}8$31Rn$2~5n7yG^Y<42_)dFcMn1^?BMnUDhb zkxF|^+e}{puKAoqq zToYafq2I#*$Pma#$S6&Cj=%95uB+zyd4A_*8cZu+o6I+T7JERdJC&V09qT1lAO_=F ztT%pJuHVr2{0%-HYk95Xvo_F4gK6c%SC4wawY%LRgXk|wpDI0{QUB+?m3C}Q)(iD5 zN#8rxGj(_X^Nlr|A4JV(y6YGZ8}T$RL)!E|WgDUYS6mz07=2TEb}m2VM8vY*l-_SH zwFW3%-qqjXud=ao#Tm!rSrw22A-T41nfJ=l;aC3zu_gD|K5o+2&9u)g7B77p%SK%L zQ{$+@5gBuoKvOVX0!3W^24hyxx5PT zZqrGxiGPIu#r;-q_^C0DaLTWRzM0wS6WbZ`KtD+K6`5?jzAJpOE~prN6a1>8$jyjr z`x~*&uGggd?J}?)a;O8A8_(vtU)NtDLLsO(?GETvJsCbrd%6vzuT&KInT5KtN)em3 zN6j6=Ur_kkwLBK>l4HMSE?X=IbK;ny^mT6TeO^hpY?X=OKNWQhsZ)9Q0qJ zy^1UUh!6fg^3O37zdf{3*G{F@gKF!%)w*!vY#lNTYrIXqNv^Moty6L5pLO8G9Oc;6 z=ZE82v(B~Z>Uwpp98uav68T5}`3AC1TT~*~`lQyg+5SZIm%H}1CLv3%cq3H~SEroy z_Nq=f^`W+nB=XNXIDFv69EZ2%TKbgFh~pnL#*RngyxSqR+3u9Dh}$;gnrhwui~cZ< zPYl{dQu#;!$Z_63r2HY2*eXseja2@dJ?rAbbc!+P)V7gS{$Wf1jQoFFt}{{BQc0as z^Dpd_T>ET_u^7-clFC2ER%`E=Ki3h{|3&%+Fy5lv=>Or>DeqW>>k-G#dCOY(tG~t`PXYSV zL0wBH*H%fLl6myTKYG^UI%r}qH;?ZveQp)M+l2FfpjaO#*R&(Hl^Ux<=8rLqJa!80 zTwU9JxLn^W+qbk+V*Pre_$81*ILeU}{#X~*O!kJJ^`ffRY3`^VuYeGvIits_^~&r6+B^JzMM_3ba= z=Z@G|TEAiUfA&};sr+MZ`9kF1AMZ-8nT1ZRxa&Pfc8Zv9jC;n|cU<>B{~zBFYoks5 zh8RyV)*I?$#EfM0e`doje*1IdHOaM+G7gEm1~#B=g!LQhSY+fWPl)xe@RNvbeI(|D zHzVufo0eGbDA%?c*0oCCgw#6T)S7K+Z(UzPWgV;3sZpn`!P@(G>{wbct?xwJolWey z$#_ZXnoYefTptU)F4)dtt7JK3zoF{Xn6uW2^^CEtwO-!Rglr4!nSXuFtz1X1#=Xi& zrxc$7mV;}>85;&`0NgcL>DNUB$2!sDIA9HJ1H^L6VOS@w*N4<8pHG+C|6wdLXvWLV zdf886$vgDEk(g()jh*>&Io7l8#`;jig^Dhd7VZ*jnz_!G>r!&T+_|?Iy!z#tvd3l_O35H{ti0v1KUBgs={^40e`l6Yob^Hml|F=D$V6 zk-)hn=#$E_d|W)Q5^H#ShF;63ecA&pQjx>I#X9BkQ#D!QIb3qt~Pd_Ha#1kO*Gq z$&U&ySgg$wWxz9yZ6!!IsWT40ZF@X7oj&sSgW4TdTURmq*L$!|$VxA_*y~~@-e>jW z+8$t;>6#4Uh`Jh&XN&oB?1MJeYxk~wm%P<&;Kf+`l3ot4{<(GJkjt!!qy9dc?6C8w=P%kHK85`e z>rt%paMwo2`Cm1kBT{pw>io5xPpnT-)^%`gipbw_Pvyj^+q>@Jn~^p42lhG+IsYR$ zo3!;C>RKA_+6|2>6>EXw$R9C8);PX!)@jJ~4fZ@V=RUbc;~Ge&x*13QSpVR!o`d|k z{GFULkn=YxXQ!MKOwE-m>n{wt8CU+mC4M{dXIy7NzAm{%)6LY{in#KJzHdG9_wCYJ zhil87OY(9=n${k;b&htV>t+|f;y_s+vxs@Tn#^Y@+9Tt{mD zOyzKsb^luKa?r6^(Oo?!0J9H%<1) z%_lg=M$CB3n!EHfaE#@`xz)n2Cw#|fqx0@CpQ$=`IP#o)%62Yol4;&{^yyDq^Ot>* z6K9ml8>I3z@@jqX@<=!SM!kWc*6XqA=%_H^Zd0m&%5gLA?fmC`aPRG zW|qI~^Ct5PF0u;DZ${=%iJCvIx0%0ePlbF=j{CL_-$~jz*a-RCy?A*`1M-vZTT8}S z#+uI#>GBzpKk$$I@1?Mh{0@}#R#La<0NAaACi^R!lE!!vu8{TvJ#>{FWvvVC#QVi{%H_XS<^jR z{?2=8M>F24CjPOZXYKE;AYuGHq#acnGRZ*Y(YH46+k86dcl=XPpU>+e<)KT@S<@g^ zn83f)I($&Qh^?6c2F!z77kEG3#CPOuq}%8}iFpq2H)K+NUQ2xtI+@LL)q6P4lEw3x zhn3U?HLYxpb9g@U&|ZJ$S6}_rqUh&?{?GCGOZ0T}{QUb0{QTWP&%gSX?Us~Dvo;3S zM~?XW(v|q@`^NR3T78aP=es_s=gYGH%LBWFJ~BW5^shU7&+k2UnKf?Kt7+bG&EIi3 z-VVZFJ7J+mp2PEH|FcO?GwT0Gjzyf-Nx#mf{q08E3mp@pO;2OHjw1B;RNtK= z9J}L>u<;nayEP>6?Jd&yOXe$T9wL;P7-jTqB@fe(kI?TwX`avej)b{B(c|OZ}A7n|K&kf5{YyM5HC&2dvdr5GESjO58 z-yinp(pHp>pD*Mmj7*9u?qeEE3*RQ%LHkSb{h~%Z%Xu)rQydDZ77r=I-{RabR)i%iG_6aIv#oT}t^b8# zWpQm1`aIuQC~ZGD=0C*qL$G5x1XgL&3U~)*`9jj?Z);c=38v3yRO{)>cB`M)|BsM= zroU3ZFXr^O2c^G!o3J??@?V%Ob8Nz)r%om|g;(W&v z$R>#12lE_%YaazmTo-j5ly=;M{$eKkj35aZ@643>gk6bl2g1iUqHRXCQ}Viy{*Gn$ zd=KFoS(D9{X>lKt9_wt0dM*0^rm3FOkJF^*`BuX87zb94A4ZCDbJx_fwZ>lg| zeY}+>e;$2N^AU6$(*LrLp*}}$Z)y5lV_%5Ag1+p&em10kV!!Iz18Eo3JY=@P4^T`zq59c)ss`zVk?% ztiDqP*(aGk_uN>1_BZr-$mV{Tf8NjcN11-W^F8>v%2i;l2MKXS~b| literal 0 HcmV?d00001 diff --git a/icons/ppt.png b/icons/ppt.png new file mode 100755 index 0000000000000000000000000000000000000000..841b67b8fcb3cb6238776a4b0115c847b6af4358 GIT binary patch literal 12417 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 JS)pzh@jvp^P)7g& literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 5a0cec6..b19415f 100755 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + ppt-control @@ -28,7 +29,7 @@ - Current: /? + Current: /?

@@ -41,6 +42,7 @@ + diff --git a/obs_ppt.py b/obs_ppt.py deleted file mode 100755 index a4e0c39..0000000 --- a/obs_ppt.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- - -import obspython as obs -# pip install pywin32 -import win32com.client -import pywintypes -import os -import shutil -import http_server_39 as server -#import http.server as server -import socketserver -import threading -import functools - -powerpoint = None -hotkey_id_frst = None -hotkey_id_prev = None -hotkey_id_next = None -hotkey_id_last = None -hotkey_id_black = None - -HOTKEY_NAME_FRST = 'powerpoint_slides.first' -HOTKEY_NAME_PREV = 'powerpoint_slides.previous' -HOTKEY_NAME_NEXT = 'powerpoint_slides.next' -HOTKEY_NAME_LAST = 'powerpoint_slides.last' -HOTKEY_NAME_BLACK = 'powerpoint_slides.black' -HOTKEY_NAME_WHITE = 'powerpoint_slides.white' - -HOTKEY_DESC_FRST = 'First PowerPoint slide' -HOTKEY_DESC_PREV = 'Previous PowerPoint slide' -HOTKEY_DESC_NEXT = 'Next PowerPoint slide' -HOTKEY_DESC_LAST = 'Last PowerPoint slide' -HOTKEY_DESC_BLACK = 'Black PowerPoint slide' -HOTKEY_DESC_WHITE = 'White PowerPoint slide' - -class Handler(server.CGIHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__))) - -def run_http(): - httpd = server.HTTPServer(("", 8000), Handler) - httpd.serve_forever() - - - - -# ------------------------------------------------------------ -# global functions for script plugins - -def script_load(settings): - global hotkey_id_frst - global hotkey_id_prev - global hotkey_id_next - global hotkey_id_last - global hotkey_id_black - global hotkey_id_white - - hotkey_id_frst = register_and_load_hotkey(settings, HOTKEY_NAME_FRST, HOTKEY_DESC_FRST, slideshow_view_first) - hotkey_id_prev = register_and_load_hotkey(settings, HOTKEY_NAME_PREV, HOTKEY_DESC_PREV, slideshow_view_previous) - hotkey_id_next = register_and_load_hotkey(settings, HOTKEY_NAME_NEXT, HOTKEY_DESC_NEXT, slideshow_view_next) - hotkey_id_last = register_and_load_hotkey(settings, HOTKEY_NAME_LAST, HOTKEY_DESC_LAST, slideshow_view_last) - hotkey_id_black = register_and_load_hotkey(settings, HOTKEY_NAME_BLACK, HOTKEY_DESC_BLACK, slideshow_view_black) - hotkey_id_white = register_and_load_hotkey(settings, HOTKEY_NAME_WHITE, HOTKEY_DESC_WHITE, slideshow_view_white) - - daemon = threading.Thread(name="daemon_server", target=run_http) - daemon.setDaemon(True) - daemon.start() - -def script_unload(): - obs.obs_hotkey_unregister(slideshow_view_first) - obs.obs_hotkey_unregister(slideshow_view_previous) - obs.obs_hotkey_unregister(slideshow_view_next) - obs.obs_hotkey_unregister(slideshow_view_last) - obs.obs_hotkey_unregister(slideshow_view_black) - obs.obs_hotkey_unregister(slideshow_view_white) - -def script_save(settings): - save_hotkey(settings, HOTKEY_NAME_FRST, hotkey_id_frst) - save_hotkey(settings, HOTKEY_NAME_PREV, hotkey_id_prev) - save_hotkey(settings, HOTKEY_NAME_NEXT, hotkey_id_next) - save_hotkey(settings, HOTKEY_NAME_LAST, hotkey_id_last) - save_hotkey(settings, HOTKEY_NAME_BLACK, hotkey_id_black) - save_hotkey(settings, HOTKEY_NAME_WHITE, hotkey_id_white) - -def script_description(): - return 'Navigate Powerpoint Slides.' - -def script_defaults(settings): - obs.obs_data_set_default_string(settings, 'cache', r'''C:\Windows\Temp''') - -def script_properties(): - props = obs.obs_properties_create() - - obs.obs_properties_add_path(props, "cache", "Slide cache: ", obs.OBS_PATH_DIRECTORY, "*.jpg", r'''C:\Windows\Temp''') - return props - -def script_update(settings): - global cache - cache = obs.obs_data_get_string(settings, "cache").replace("/", "\\") - -def register_and_load_hotkey(settings, name, description, callback): - hotkey_id = obs.obs_hotkey_register_frontend(name, description, callback) - hotkey_save_array = obs.obs_data_get_array(settings, name) - obs.obs_hotkey_load(hotkey_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - return hotkey_id - -def save_hotkey(settings, name, hotkey_id): - hotkey_save_array = obs.obs_hotkey_save(hotkey_id) - obs.obs_data_set_array(settings, name, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - -#------------------------------------- - -def get_slideshow_view(): - global powerpoint - - if powerpoint is None: - powerpoint = win32com.client.Dispatch('Powerpoint.Application') - - if powerpoint is None: - return - - ssw = powerpoint.SlideShowWindows - if ssw.Count == 0: - return - - # https://docs.microsoft.com/en-us/office/vba/api/powerpoint.slideshowwindow.view - ssv = ssw[0].View - - return ssv - -def get_activepresentation(): - global powerpoint - - if powerpoint is None: - powerpoint = win32com.client.Dispatch('Powerpoint.Application') - - if powerpoint is None: - return - - activepres = powerpoint.ActivePresentation - return activepres - -def export_next(slide): - global cache - ssp = get_activepresentation() - if ssp: - if slide < len(ssp.Slides): - ssp.Slides(slide + 1).Export(cache + r'''\slide0.jpg''', "JPG") - attempts = 0 - while attempts < 3: - try: - os.replace(cache + r'''\slide0.jpg''', cache + r'''\slide.jpg''') - except: - pass - attempts += 1 - else: - shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\blank.jpg''', 'rb'), open(cache + r'''\slide.jpg''', 'wb')) - -def slideshow_view_first(pressed): - if pressed: - ssv = get_slideshow_view() - if ssv: - ssv.First() - ssv.State = 1 - -def slideshow_view_previous(pressed): - if pressed: - ssv = get_slideshow_view() - if ssv: - ssv.Previous() - ssv.State = 1 - export_next(ssv.CurrentShowPosition) - -def slideshow_view_next(pressed): - if pressed: - ssv = get_slideshow_view() - if ssv: - ssv.Next() - ssv.State = 1 - export_next(ssv.CurrentShowPosition) - - -def slideshow_view_last(pressed): - if pressed: - ssv = get_slideshow_view() - if ssv: - ssv.Last() - ssv.State = 1 - -def slideshow_view_black(pressed): - if pressed: - ssv = get_slideshow_view() - if ssv: - if ssv.State == 3 or ssv.State == 4: - ssv.State = 1 - else: - ssv.State = 3 - -def slideshow_view_white(pressed): - if pressed: - ssv = get_slideshow_view() - if ssv: - if ssv.State == 4 or ssv.State == 3: - ssv.State = 1 - else: - ssv.State = 4 diff --git a/ppt-control.js b/ppt-control.js index 7e4c312..e1b8841 100644 --- a/ppt-control.js +++ b/ppt-control.js @@ -1,4 +1,6 @@ +var DEFAULT_TITLE = "ppt-control" var preloaded = false; +var preload = []; function imageRefresh(id) { img = document.getElementById(id); @@ -108,6 +110,11 @@ function sync_next() { } show_next.onclick = sync_next; +function sync_shortcuts() { + saveSettings(); +} +shortcuts.onclick = sync_shortcuts; + function set_control_width() { var width = window.innerWidth || document.documentElement.clientWidth @@ -154,10 +161,27 @@ document.addEventListener('keydown', function (e) { } }); +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function disconnect() { + document.title = DEFAULT_TITLE; + current_img.src = "/black.jpg"; + next_img.src = "/black.jpg"; + users.textContent = "Connection to PowerPoint failed"; +} + websocket.onmessage = function (event) { + console.log("Received data"); data = JSON.parse(event.data); switch (data.type) { case 'state': + if (data.connected == "0" || data.connected == 0) { + console.log("Disconnected"); + disconnect(); + break; + } var d = new Date; switch (data.visible) { case 3: @@ -194,14 +218,13 @@ websocket.onmessage = function (event) { console.error( "unsupported event", data); } - if (!preloaded) { - var i = 0 - var preload = []; + if (preloaded == false && ! isNaN(total.textContent)) { + image = document.getElementById("preload_img"); for (let i=1; i<=Number(total.textContent); i++) { - image = new Image(); image.src = "/cache/" + i + ".jpg"; preload.push(image); - console.log("Preloaded image " + i); + console.log("Preloaded " + total.textContent); + //sleep(0.5) } preloaded = true; } @@ -211,6 +234,7 @@ websocket.onmessage = function (event) { var interval = setInterval(refresh, 5000); function refresh() { + console.log("Refreshing") websocket.send(JSON.stringify({action: 'refresh'})); } diff --git a/ppt_control.py b/ppt_control.py index 75cf1b0..2e0cc3d 100755 --- a/ppt_control.py +++ b/ppt_control.py @@ -15,9 +15,15 @@ import urllib import posixpath import time import pythoncom +import pystray +import tkinter as tk +from tkinter import ttk +from PIL import Image, ImageDraw logging.basicConfig() +global STATE +global STATE_DEFAULT global current_slideshow current_slideshow = None CACHEDIR = r'''C:\Windows\Temp\ppt-cache''' @@ -50,6 +56,9 @@ class Handler(server.SimpleHTTPRequestHandler): if len(words) > 0 and words[0] == "cache": if current_slideshow: path = CACHEDIR + "\\" + current_slideshow.name() + else: + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "black.jpg") + '/' + return path words.pop(0) else: path = self.directory @@ -67,11 +76,13 @@ def run_http(): http_server = server.HTTPServer(("", 80), Handler) http_server.serve_forever() -STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""} +STATE_DEFAULT = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""} +STATE = STATE_DEFAULT USERS = set() def state_event(): + print("Running state event") return json.dumps({"type": "state", **STATE}) @@ -80,12 +91,15 @@ def users_event(): async def notify_state(): - global current_slideshow - if current_slideshow: + print("Notifying state to " + str(len(USERS)) + " users") + global STATE + if current_slideshow and STATE["connected"] == 1: STATE["current"] = current_slideshow.current_slide() STATE["total"] = current_slideshow.total_slides() STATE["visible"] = current_slideshow.visible() STATE["name"] = current_slideshow.name() + else: + STATE = STATE_DEFAULT if USERS: # asyncio.wait doesn't accept an empty list message = state_event() await asyncio.wait([user.send(message) for user in USERS]) @@ -108,6 +122,7 @@ async def unregister(websocket): async def ws_handle(websocket, path): + print("Received command") global current_slideshow # register(websocket) sends user_event() to websocket await register(websocket) @@ -150,19 +165,22 @@ async def ws_handle(websocket, path): current_slideshow.goto(int(data["value"])) await notify_state() elif data["action"] == "refresh": + print("Received refresh command") + await notify_state() if current_slideshow: current_slideshow.export_current_next() current_slideshow.refresh() - await notify_state() else: logging.error("unsupported event: {}", data) finally: await unregister(websocket) def run_ws(): + # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084 + # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/ pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED) asyncio.set_event_loop(asyncio.new_event_loop()) - start_server = websockets.serve(ws_handle, "0.0.0.0", 5678) + start_server = websockets.serve(ws_handle, "0.0.0.0", 5678, ping_interval=None) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever() @@ -200,71 +218,120 @@ class Slideshow: if self.instance.ActivePresentation is None: raise ValueError("PPT instance has no active presentation") self.presentation = self.instance.ActivePresentation - - self.export_all() + + def unload(self): + connect_ppt() def refresh(self): - if self.instance is None: - raise ValueError("PPT instance cannot be None") + try: + if self.instance is None: + raise ValueError("PPT instance cannot be None") - #if self.instance.SlideShowWindows.Count == 0: - # raise ValueError("PPT instance has no slideshow windows") - self.view = self.instance.SlideShowWindows[0].View + if self.instance.SlideShowWindows.Count == 0: + raise ValueError("PPT instance has no slideshow windows") + self.view = self.instance.SlideShowWindows[0].View - if self.instance.ActivePresentation is None: - raise ValueError("PPT instance has no active presentation") + if self.instance.ActivePresentation is None: + raise ValueError("PPT instance has no active presentation") + except: + self.unload() def total_slides(self): - return len(self.presentation.Slides) + try: + self.refresh() + return len(self.presentation.Slides) + except ValueError or pywintypes.com_error: + self.unload() def current_slide(self): - return self.view.CurrentShowPosition + try: + self.refresh() + return self.view.CurrentShowPosition + except ValueError or pywintypes.com_error: + self.unload() def visible(self): - return self.view.State + try: + self.refresh() + return self.view.State + except ValueError or pywintypes.com_error: + self.unload() def prev(self): - self.refresh() - self.view.Previous() + try: + self.refresh() + self.view.Previous() + self.export_current_next() + except ValueError or pywintypes.com_error: + self.unload() def next(self): - self.refresh() - self.view.Next() - self.export_current_next() + try: + self.refresh() + self.view.Next() + self.export_current_next() + except ValueError or pywintypes.com_error: + self.unload() def first(self): - self.refresh() - self.view.First() - self.export_current_next() + try: + self.refresh() + self.view.First() + self.export_current_next() + except ValueError or pywintypes.com_error: + self.unload() def last(self): - self.refresh() - self.view.Last() - self.export_current_next() + try: + self.refresh() + self.view.Last() + self.export_current_next() + except ValueError or pywintypes.com_error: + self.unload() def goto(self, slide): - self.refresh() - if slide <= self.total_slides(): - self.view.GotoSlide(slide) - else: - self.last() - self.next() - self.export_current_next() + try: + self.refresh() + if slide <= self.total_slides(): + self.view.GotoSlide(slide) + else: + self.last() + self.next() + self.export_current_next() + except ValueError or pywintypes.com_error: + self.unload() def black(self): - self.refresh() - self.view.State = 3 + try: + self.refresh() + self.view.State = 3 + self.export_current_next() + except ValueError or pywintypes.com_error: + self.unload() def white(self): - self.refresh() - self.view.State = 4 + try: + self.refresh() + self.view.State = 4 + self.export_current_next() + except ValueError or pywintypes.com_error: + self.unload() def normal(self): - self.refresh() - self.view.State = 1 + try: + self.refresh() + self.view.State = 1 + self.export_current_next() + except ValueError or pywintypes.com_error: + self.unload() def name(self): - return self.presentation.Name + try: + self.refresh() + return self.presentation.Name + except ValueError or pywintypes.com_error: + self.unload() + def export_current_next(self): self.export(self.current_slide()) @@ -279,7 +346,6 @@ class Slideshow: while attempts < 3: try: self.presentation.Slides(slide).Export(destination, "JPG") - time.sleep(0.5) break except: pass @@ -302,22 +368,74 @@ def get_ppt_instance(): def get_current_slideshow(): return current_slideshow - -if __name__ == "__main__": - - start_server() - +def connect_ppt(): + global STATE + if STATE["connected"] == 1: + print("Disconnected from PowerPoint instance") + STATE = STATE_DEFAULT while True: - # Check if PowerPoint is running - instance = get_ppt_instance() try: + instance = get_ppt_instance() + global current_slideshow current_slideshow = Slideshow(instance) STATE["connected"] = 1 STATE["current"] = current_slideshow.current_slide() STATE["total"] = current_slideshow.total_slides() print("Connected to PowerPoint instance") + current_slideshow.export_all() break except ValueError as e: current_slideshow = None pass time.sleep(1) + +def start(_=None): + #root = tk.Tk() + #root.iconphoto(False, tk.PhotoImage(file="icons/ppt.png")) + #root.geometry("250x150+300+300") + #app = Interface(root) + #interface_thread = threading.Thread(target=root.mainloop()) + #interface_thread.setDaemon(True) + #interface_thread.start() + start_server() + connect_ppt() + + +def null_action(): + pass + +class Interface(ttk.Frame): + + def __init__(self, parent): + ttk.Frame.__init__(self, parent) + + self.parent = parent + + self.initUI() + + def initUI(self): + + self.parent.title("ppt-control") + self.style = ttk.Style() + #self.style.theme_use("default") + + self.pack(fill=tk.BOTH, expand=1) + + quitButton = ttk.Button(self, text="Close", + command=self.quit) + quitButton.place(x=50, y=50) + status_label = ttk.Label(self, text="PowerPoint status: not detected") + status_label.place(x=10,y=10) + + + +def show_icon(): + menu = (pystray.MenuItem("Status", lambda: null_action(), enabled=False), + pystray.MenuItem("Restart", lambda: start()), + pystray.MenuItem("Settings", lambda: open_settings())) + icon = pystray.Icon("ppt-control", Image.open("icons/ppt.ico"), "ppt-control", menu) + icon.visible = True + icon.run(setup=start) + +if __name__ == "__main__": + show_icon() diff --git a/ppt_control_obs.py b/ppt_control_obs.py new file mode 100755 index 0000000..d9ab70d --- /dev/null +++ b/ppt_control_obs.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- + +import obspython as obs +import asyncio +import websockets +import threading + +hotkey_id_first = None +hotkey_id_prev = None +hotkey_id_next = None +hotkey_id_last = None +hotkey_id_black = None +hotkey_id_white = None + +HOTKEY_NAME_FIRST = 'powerpoint_slides.first' +HOTKEY_NAME_PREV = 'powerpoint_slides.previous' +HOTKEY_NAME_NEXT = 'powerpoint_slides.next' +HOTKEY_NAME_LAST = 'powerpoint_slides.last' +HOTKEY_NAME_BLACK = 'powerpoint_slides.black' +HOTKEY_NAME_WHITE = 'powerpoint_slides.white' + +HOTKEY_DESC_FIRST = 'First PowerPoint slide' +HOTKEY_DESC_PREV = 'Previous PowerPoint slide' +HOTKEY_DESC_NEXT = 'Next PowerPoint slide' +HOTKEY_DESC_LAST = 'Last PowerPoint slide' +HOTKEY_DESC_BLACK = 'Black PowerPoint slide' +HOTKEY_DESC_WHITE = 'White PowerPoint slide' + +global cmd +cmd = "" + +async def communicate(): + global cmd + async with websockets.connect("ws://10.0.0.93:5678", ping_interval=None) as websocket: + while True: + if cmd: + await websocket.send("{\"action\": \"" + cmd + "\"}") + cmd = "" + await asyncio.sleep(0.05) + +def run_ws(): + asyncio.set_event_loop(asyncio.new_event_loop()) + asyncio.get_event_loop().run_until_complete(communicate()) + + + +#------------------------------------------------------------ +# global functions for script plugins + +def script_load(settings): + global hotkey_id_first + global hotkey_id_prev + global hotkey_id_next + global hotkey_id_last + global hotkey_id_black + global hotkey_id_white + + hotkey_id_first = register_and_load_hotkey(settings, HOTKEY_NAME_FIRST, HOTKEY_DESC_FIRST, first_slide) + hotkey_id_prev = register_and_load_hotkey(settings, HOTKEY_NAME_PREV, HOTKEY_DESC_PREV, prev_slide) + hotkey_id_next = register_and_load_hotkey(settings, HOTKEY_NAME_NEXT, HOTKEY_DESC_NEXT, next_slide) + hotkey_id_last = register_and_load_hotkey(settings, HOTKEY_NAME_LAST, HOTKEY_DESC_LAST, last_slide) + hotkey_id_black = register_and_load_hotkey(settings, HOTKEY_NAME_BLACK, HOTKEY_DESC_BLACK, black) + hotkey_id_white = register_and_load_hotkey(settings, HOTKEY_NAME_WHITE, HOTKEY_DESC_WHITE, white) + ws_daemon = threading.Thread(name="ws_daemon", target=run_ws) + ws_daemon.setDaemon(True) + ws_daemon.start() + print("Started websocket client") + +def script_unload(): + obs.obs_hotkey_unregister(first_slide) + obs.obs_hotkey_unregister(prev_slide) + obs.obs_hotkey_unregister(next_slide) + obs.obs_hotkey_unregister(last_slide) + obs.obs_hotkey_unregister(black) + obs.obs_hotkey_unregister(white) + +def script_save(settings): + save_hotkey(settings, HOTKEY_NAME_FIRST, hotkey_id_first) + save_hotkey(settings, HOTKEY_NAME_PREV, hotkey_id_prev) + save_hotkey(settings, HOTKEY_NAME_NEXT, hotkey_id_next) + save_hotkey(settings, HOTKEY_NAME_LAST, hotkey_id_last) + save_hotkey(settings, HOTKEY_NAME_BLACK, hotkey_id_black) + save_hotkey(settings, HOTKEY_NAME_WHITE, hotkey_id_white) + +def script_description(): + return "ppt-control client\nHotkeys for controlling PowerPoint slides using websockets" + +def script_defaults(settings): + obs.obs_data_set_default_int(settings, 'port', 5678) + +def script_properties(): + props = obs.obs_properties_create() + + obs.obs_properties_add_int(props, "port", "Websocket port: ", 0, 9999, 1) + return props + +def script_update(settings): + global port + port = obs.obs_data_get_int(settings, "port") + +def register_and_load_hotkey(settings, name, description, callback): + hotkey_id = obs.obs_hotkey_register_frontend(name, description, callback) + hotkey_save_array = obs.obs_data_get_array(settings, name) + obs.obs_hotkey_load(hotkey_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + return hotkey_id + +def save_hotkey(settings, name, hotkey_id): + hotkey_save_array = obs.obs_hotkey_save(hotkey_id) + obs.obs_data_set_array(settings, name, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + +#------------------------------------- + +def first_slide(pressed): + if pressed: + global cmd + cmd = "first" + +def prev_slide(pressed): + if pressed: + global cmd + cmd = "prev" + +def next_slide(pressed): + if pressed: + global cmd + cmd = "next" + +def last_slide(pressed): + if pressed: + global cmd + cmd = "last" + +def black(pressed): + if pressed: + global cmd + cmd = "black" + +def white(pressed): + if pressed: + global cmd + cmd = "white" diff --git a/settings.js b/settings.js index eb94f8e..901af0b 100644 --- a/settings.js +++ b/settings.js @@ -25,16 +25,17 @@ function getCookie(cname) { } function saveSettings() { + console.log("Saving settings") settingsString = JSON.stringify({showcurrent: show_current.checked, shownext: show_next.checked, enable_shortcuts: shortcuts.checked}); setCookie(COOKIENAME, settingsString, COOKIEEXP); } function initSettings() { if (getCookie(COOKIENAME) == 0) { - if (window.obssstudio) { - shortcuts.checked = False; - show_current.checked = False; - } + if (window.obssstudio) { + shortcuts.checked = False; + show_current.checked = False; + } saveSettings() } else { cookie = JSON.parse(getCookie(COOKIENAME)); -- 2.47.1