From b6fce38b8bd400517639c163bfbfa7d1505841e6 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:31:36 +0200 Subject: [PATCH] release: Multi-Hoster-Upload 2.1.1 Add live English and German interface switching, animated navigation and history controls, improved update changelogs, remembered upload folders, reliable repeated hot reloads, and refreshed public documentation. --- README.md | 3 +- assets/product-overview.png | Bin 46806 -> 46480 bytes lib/config-store.js | 16 + lib/updater.js | 33 +- main.js | 26 +- package-lock.json | 4 +- package.json | 2 +- renderer/app.js | 428 +++++++++++++++++++--- renderer/i18n.js | 587 ++++++++++++++++++++++++++++++ renderer/index.html | 55 ++- renderer/styles.css | 314 +++++++++++++++- scripts/dev-runner.cjs | 6 +- scripts/verify-public-release.mjs | 3 + tests/config-store.test.js | 14 + tests/dev-runner.test.js | 11 + tests/i18n.test.js | 27 ++ tests/startup-renderer.test.js | 13 + tests/ui-smoke.js | 185 +++++++++- tests/updater-version.test.js | 19 +- 19 files changed, 1658 insertions(+), 88 deletions(-) create mode 100644 renderer/i18n.js create mode 100644 tests/dev-runner.test.js create mode 100644 tests/i18n.test.js diff --git a/README.md b/README.md index e144ad0..e9aa0b7 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ Multi-Hoster-Upload is a Windows desktop app for managing large file batches acr - Manage multiple accounts per hoster with validation, health checks, automatic rotation, and inline OTP completion. - Filter uploads, accounts, and history from task-focused sidebars without changing the underlying queue. - Add files by drag and drop or file selection and monitor live queue progress. +- Use the complete interface in English or German and switch languages without restarting the app. - Control per-hoster concurrency, bandwidth limits, retries, folder monitoring, notifications, and completed-item cleanup. -- Keep local upload history and copy completed links in bulk. +- Keep local upload history, choose a retention period, and copy completed links in bulk. - Transfer accounts and settings with a 75-character encrypted online key while encryption and decryption stay on the client. - Check for updates from Settings or Help; when a newer release is available, install it from the header update action and accessible update dialog. diff --git a/assets/product-overview.png b/assets/product-overview.png index 514cdda3367e26fd8bb982a31a6fe36a6ecb953a..799d2e378eac3739a70684382808c12d196ac642 100644 GIT binary patch literal 46480 zcmc%xcT`hb^goJ<9KeF0N0dlaP!Z|UYi#r;qJmO_L@7c*1f+)G@hAuZ!~!TKDov${ zKtc%+gd_?ALX;LDN(2loh899Xa(BSfzW059?~V7zy^O&i?7i38bIvu_ET1`7?wxV4 z+_6n++onyMc34}TKD%j?u;!*sf-PHwfh%XyspG&OfzY#-CpVRM$`3ceem$zhY+4tVcR_xFFTJ~Bek~?;V;||Cr zVsa8b^xW1?5`6*Q6J=n6Q~nycO|_)-)vC$TNKD1t7fNG8RSffxN~7ni=shC=WsT~_ zF@>+_4gHsR-Er($Nq|7$=lneH`|gd8H}wSs*FU|K`TyZV&ep9Uc2mTKnT5IA)Z85+ zcw?sNk>w#KZXkd%$ViPiR5d2c#)3zix<5v;M?bK=QE-YRefEv|`YYl?M3531%W;kn zH|)S;AONS97bIY92<`hTN4IA>} z$*0T)3bzQhdgV!ce8{J-u>9qb3Jsq)PJ1L15?89`Ib$5*|8Zqt5{hEV8GtlK=Tn!L z_IM)JmR9-^YyD{2htO`*{`-#IYgFJJiPKgj<1%W&Lx*dD#;Cla!YJ0_tIz)Oy&(nE zkC~mO+!g8$)}m+1^5|whb)PS8laOEgVF~TZF_cxi__m_gy=n7)ZY-3Ci+59WD%gs?S$b&v;%sGBPJXqXz0a?6=S>~uuiJ2EOQVo7ZuQWC*P zWunU}?h8+oP_Z~OIEovVBwjbapS{!%p?nEAtmtL;*$Yt&-w7x_@bmi6n)P!9RG!8| z$C5uBp1&YfztWtGcWO%xBfo4~pr4=$GsOBAu$y_Ow)I3-rWLPe~UBJQ4C~l=sHs#nQnI+ z=SN5$dm827twu0zk^0+|cnsRzx?+#rbl#$U$dB`BV#xq2%MqA$R-D7$Lr<oT^rS-5Sw;xfsi%IYz~>%reuA;T|02B1!sHtqLq&H&dH5_l|HsKrqII;LU^;`{OuJk|gccNQ#Q4 z*;)kS+xB4Ob}lPohb5#|&nNe7`94h>##lE69ZX)`hWCOSnuI;gAl|yS1(d6Bx^mWG zVF9ihtn`6d8M}0fp``YZ4lU1)cS;o+hpWj4$9aDzp5r>tL^RFLYdx;`c!Z1G8@4W( zqN+t-U;#-Jd(VzXGSH2_Z6BL2?q;hQDvl!BAmYNvC)-m~BILuPWqy7rNx&zAW5^AB zT@cSIt{}xnY1KmT@lK-&V>U?+Y$s2brj%IuP=C+3aEqu*#qmz1!7c*%uIg*b%VmHF zv?c`BoLTV#I+Ffcc;kz`|Wzjcd1ht%qH!M{?Zd{hn4v+c z08P7dXS<|c#)o2hQ2w2yo(cO5h6qxOF;)iHAGTbiYwUIW>y@ub-NXi;&S?;zGIlkf z%8liiA*h55K-J6VvGbG@s}Gk03Yfq1$@4Pe8#}36|5&!QhMPLU%YeJNdJ1 z`eBg-VpWg$4+}t1@N5BSDPs)Wc$FSW990CjWmoRrupe3KfC(RMpu|-&o7hA*${-w= z>jE~C9z*W8r2i(mK2=e2h{(pL_(toZ-ehpLWvhA(2-cVm*>N9nr^e|fg7 z8&PTRw{F-Q+@p?|(+;?ehl<=s6>sd8(_9|(MH#Goy$x;oL#WcP=Y_%4%lu6Pf`y1y zQu8>CvbY!=!=;-7W6pRG{GV*2%7WWtP6wS7v>5uJm};OPX1e$(%QWWZ$1E+ucvII^ zjLBMd9v9}wBTVw<_+Iu4G$-SgP97s4Yx(`KsuyP*!o7N`X37Yu=R?loK4iJBG@GRRAT{Rns})f z@O<&FF6U$%1$C^C1&kA|=>o0yeyq~1d%A3#p!BZGj0fVxcXuoaoYuWVf!C)=U1 zcjs)`_m>Y*z*CBA*%X+$2r#JVZpfwA1>|+^fF||LpJK!f~;N^q$ZN6C99V3?1NFhnmW0@_Ct_XPv z`Y`hu5S$3Q`4BznPi5(TS$pUMlWRMIDZW0WQChS1*eqLSC%b7fK9V9ZOTJ~=={I>c zI&t}&*@CxAZ&Lxho%wLp3f2D?y+}Q@&X7r#>Ms%s1r+hga7v8Vj5bI;I$1K99AiN; z7Ei>`;m5HH9`t0U?Dga1WiQ8LLv@Qi(6oT$Ee&o%#j)U5X+d$D_#$xjt*$KWnP|B} z^Whw!@2XSY0qRuIut?*xaI2DntgEDatgGY!zZB#ts;S6F8c5>-rfC7rKu0ubEW?@X zA+$)$gPyKK0=Hk&CPetlrx|V`?Is12v&#q~@Ru;VH$FW`TK6n=fNd&*Z(=Xux!T}Q zTlS3ufz?pN1uQfE6tp6u5j@H$f6_W;-s;IFl1J+hzvw>bbw+oDvxy&(6m|JQZSy;C zf9UO8S2AZQO^fIWe2kcZ&S)bqCz8ocKt_^ZHje75@MF3$54x%5avZbtGULVlVDaUH z`MI{6Ey8o6CZ2?hg!k`wsvqo0J{JC|Q+z)#x!2T&a1Rq_2CJZqF+8IuEv}kDS%f!z z4D0{_c!MIgQ@3 z7rhJW%ZArOu=Igs?+WH(3J@AL(Qj4qEW6SgebkVR6m6B|=>CtF8Gwn64E`l*9VY>5 zxd_+SWSPWE6lr3gBx5U|zK?ejMHk;<>I`_E{zFH@-j+fyPfU;dy2s^Wfqbk#Bq_=~ zOc`=V>x;`33HG_R#|4oZ&ec|IS8TCAWTBt=I$QoYAYI(&eXv0P>L`+vmd`^+RA4bG zE*dK8)A>}lGH~$i3QojqXO&OqHI6}s;Lo7H9743n5ku+R03r++2Ci6vGf8nj>!i6eDhAtf!36p>h`B` zRt$wZ$qHUbeQ&#SIIG(;_)W1|WuQkIHc^)j?`WT_$PF|%Q7*r7B;oPF*-(~Eg7@Gn zBnTgK-o5cbx5gu8xWaP9e>oXBLhvD6dqLmgbb-_BuM~wlQZzZ8Wo3>v7=Pq^*WMAT zKMpHqh2I1Hvscq3X=o=}d$OWlBS@Z;z>{6*H{BOA3)Wwb?ry1E32q+x$q%0Ck@TD_ z1c`?f4~jdKINTV@p9?>lHDWkQa>GY+>x|gyFRl*^2x%bOsaWQ0}Gv;s5KI-^krjHG#u~}1+0*m z=kE(mwc4q*)4cvIAj`nm17b$L&WmfEg$zavxA8gMIg@b0HyEinx5CpCBos+PM~9RE z-IUi-^$e5OoJ%g|trV}EnjcD|qIb z7Db_#6DssPnTzm3*a&mx>*@{s4OX;K!0dvGu{X>=n%Ez=(p$Tb(2eg1Mx&P|q#5(t zX~f*gjQ$7*_A$rN-6&a;KCvVi<&C0e*Q9AEB(za>qLJHM#aL?g=>3-UU~ZJ`QI5%i z%vfG@p<9yF9I-u@c%h7?J%b!E_Rj1f`Ol;DEOl=MgRb55RmknYpg-DoRJ!cj+CvLn>i zpPfs1(mPKJXVo9>eGuA5AgFc5zx|eURZBmcf?4^~HHunadp(35;44$RKO?WwNYA|y z3-`>d-?tt@BncAEV6DVw;#?>RRJg51YF!CNcQb+Q=qW9t-mGr63gLnoAJ}o^`%QW9 z-lvf~Yki%~gkhCtS$3iC<&S1JT(Lity*La<#zQsy*Hs#02_j~Xx*yMArV&VvrqYh! zC2#DgD_xoqOJ(~~zctgbd0q88K$C4QDE}Srcw9W;X59JT?ooKEbDZm;buDKW+Cx*W z<{`!%85}4iai6w&N`+a^a5JRwbM%O9=heKpv8LLCuzb*aL|Gm8ZZP6i-J194c5rG1 zc*T#?LBGVF?+7Nmf)O8uF-Iq+*SH$l^d))E$k-LL8X@G|Xxd$I%5Jc-0ufNK?%;;7 z6(DxydIZRywPRsAD+^`^2tj}mHxbBB+=K5o$Gx9S`Epta>3zJh_QvxSk;at+&at`T zZH{P#(~qVjD=VxSr(VizP0G4oG(MeuO;HGWMWOuNkN+-U*dxlxYsWhCOdGU>ALfNP z<>jFvryn{9D5{}6Gd)oiq23=;pc!)es!09XllZ~uRq8uM+l90y%~iGw@#)fy;NZ`*ET@7b?kIfjg%!FR>mDXc4@XLzh&h|w4>CdA|v{ZvO{ z<+E1o7(1{z0T4R6jJcZ<$nn_Ggko_rf+%T(w`C<)c$oS`m({L73xJ&@w~9%WBJ)9X6pE4y>j zbeh16wc2BU&^Z;gUyU`|o)h7RN6}1yO6CE1IZ;sV7#`IOqo8NQ`;r8@P2?z1&vmzO zzwHb8fR<1Pc;vk@yzlWLo-t9vC>eX55H{Wxkjp%fCT1%a=n>K*6$_ac2M2iUIy-#U z-%|7nCQ8cR1LmS&vJ}YaXe`^Q`EC!W8)57*k}mJuI}(#!5%F^y(swSvbFk-tI`XC{ zi>(#5WWTDadhZq@fKuE=uGMRz)SewrS;-{NL!}#fx(aD>ny+842sm?avtcSFH+tw2 z28W%8n$yvl%Z9Tt#gict3xoym@BfWDrY-clbADWTt?+JckslC+l=A&_c4k3TZn>~^ zM{yKwiG}r)3H3*ma<8mLpU8=zp806Cx71a7TElH<`RQrg5t;-Ld_)*}$?Q(xeECqx z>EKDIAbv)-et>W<$Z%Yg2&3@A_?`ovNK>z2{YaHU2%K8v^U(@pbZzZ0_lE$~Isn&T zDu>kf(rFHs8cO(vX_P95%Uj62LA(xI!i6*2BJv{DJ-P4z(?PYz7MJ?J9*_I*^_c90 zDR01D(Q~|KHinEImGH=0^>KHe2`1KR&TCaS%4dQ{u~L(TD15&>$0v5pva{s6C*nnT zKY$~t3 z#ypvs+J;PzV}*HF;IejU@6C~%b`CV~89!5+Rky#Z77gypi?Ca`a8`B;=n$Y=e0WkM zx8c<;rBQ7jG-P1wr2Y%@U78M4JfDxe>;-R1GiUk&t~MWvU8%w}mH-=khY)g~$|a1# zwGoFY7m5M=XAAk%?a^XPkl~^Q=Zf?6(L3V7sVJTDI8c4~C&0c@yBl@X>q===#d}@M z!=kpo8hie2Q0-xoZ;VcAMI}!@joN`Zx9TCc%R-JrH@QzS3;uA3f-IH}xl&zXTOSi_ zIWlo%p#csqk1XA%I`l|>e)O_5G5DIthxv(a?`Is76uzrm`dxD_`YS?D?yEfo&9 zQ-~M{A8h}xG32nqAP2gqNyO%eM-vks8hedJ&)!J zvR_n$g~W(+`G5KpF&ImjXE_lP_J#pE$g2u%-TN9TrMMt;}v4R zS7fU;a<0#f>(>1@ADn&F3$kZP>X9NX8DD48zfWf!0YKlV(u-9^u;>SbB%_qwh2x~@ zc^b7RVrU3d%fh;TT8XoEA3&|W{b_FNO?#bEIftz4G;rcx9kY1pDpfM)D(M@$|4zz$ zfMuNxRO0WTINy04F-we?;oXXyoKd9000e&-fX&hImHk{eBLhe?N3}vvV3$ncvQ+b6 zf&9g1lVg`hWdU@uY|R+4;*RC`1pI7Z3@2JrGaNyMN1h0j`s3{ysdWFz8d zA|~Z1;HnzKQNOa)SN-ulut!z``OY(-&PYLvB*78Rrz4ih)f_A4jvP_^7YQK1xnAvN zv!^@zeZTf#Qi-|6Hq|?ogw&p}1;H%<-C0KYDQYQt71H>ukFK-VO1(|y4bCDDj7PAJ zbo6VbJAl^~cPUz2+@;Yw&%)A*ve!YHB0aAe1AwBj`9|p66Gy@RB+%Zp% zlE7`#VE_uz5TIfo>+1wbF5w~|1BKP@m&_nUzoMgFkAlDGd=XGo<~_3~JA%EE*>Vft4+-wouWnS;`S+Z}m#x^%d zrA7rv$41(c5AJA^6r3Ngh{#+#b$Crv^^ca*LbvYSA0;WAD}bw%%PBXZ-ZRVp8@7<} za~yo{+c3A6i-F1WwRQ*WQFFi=P5(k<#6X?bwR$$d&6Q-#R?y6#g>h!|R?tHjRsiaF z-lQS_nxcn-|nEcq_nujd_J!WSBo0{#%3z%F=7B> zWXj+hwEBpdwer7H%+12*N&iJh{slcz;UXUsBbufGC}?Xc06k56Fq)XMpU|cQ50N+3 zw}Q^u|Cb1_w_)0GkC=(WkA#um;UY3S>Cq8MptATLDAtN+zg{Ful(Z88JnlNo2%v=3 z=6{YHnzj8u0#*R#Zl1iiW)`GbX%@B!!2v;#|S56`_QP*r&jgBYh)D91W@ixH|43-=d3a7085IomrU%>mXIIM6VS8!H7k4f zh+E3l6sR4xtEm<*Z{lNxajO_GFN!~K#_T^h=728>K9;DM>PH5k%uchPI12+S7jS`_ zf&I9)Rxi3&kAxr*VR%g#!XWlJfyG414+U1zYoHK7i*@0=4`NiPq zRZJuK27mS$PXL9sp>Cv*01EwJF5vOaW2Tp50T`8!j0(IFvEkh^j_@%=xvlppGQ!ml}>(@x;0BXHi_}rt73G95=0rxXboB7rAO`5sD6iQyl%{MT3Tv*2X ztM(l7+8BKC|B1mP7jI0lLOxubY-7ky@Bvs&Ru_*`KQRBv5lViDKoa9tV6HOvC()N$B0J$& zx{j8>)_^M?;j$CmiThNV_BCId=|+t$Dd}Un#-cpTCnq{Ab8hI8fTl^UcuOWp!#Bfo4EpCr|Nj}n7BB21%zq~7qT(u~E%V&9HE0@THoDd%8O zgom;emebg`+T0&I($O3PM3+-_k#(+2ZKm!iTtaXXz4=_yj*7<^$~Ta3_%={$f6X+M zRy~Xwqp^E$6oxmlETGN3{oTRrlL>IX++U`}K0_FFq&40r(B}o#jHJ2^d^Wbr=ST+* z<2NM4bfDhm+iqqX2_Mf5mV^Cy7Y)Ea;M9KG>1&=LAr7%?ndVy}^_o_{NgQ!VbGCJH zQ;qsrcX`@yNgB^AoJFxaSI#SkRm($rvqPK?x zEhfWj=rLUQB$RuW(-APfX77^DQElR4Y>j64yXZLl`)v;CV%&_7K)$~THS3qnp&Bws zWw~-(`+!etHS$DQVU#VJ6L?*dON+@S8HrD1_Gz8O)m|Ydm(eY^g9^!-jq~m%Yu{5N zS1j?;0q?z4Po0~NgNceSc%kAGe#`2vpDSu{vykNhNQNx^r@^`|>$CRr!ZPGz zPT>6ht2TG{F!7|;pnjd9);?AhXys-|@~h`d zs&$N>$oehf!A-lGqi3=i8j7Yw=$VB0^w_bXmDPu>!R*hGH}T=QsfUawT=J6X!AIq$ za!1UjI`zlKedW@OrClBXL~r`+V)h<$vLOm|J2r)2tZ^V8{@?Of+BjRw! zMFR(A*81~y5oHXjyT<#A`Z2%->88k=s!s-=BIch?)Jp>1B_U^L3H@4GgD#>)xg;ME zI$aY{mbRVunp6LLDTv$qxH2$^W{}~rFPntzEP-CEEPIFD?fs=w+Iu0RFCf@$pDk$- zFKL1B&mD*U`4&)G=oc-!nOznetvPnDWA7v0Rc|IRBEqv=j;#q2K(}wlYgJ~re+Ema ziWw^Q>Aq_{Uh38(sqLehtfDX8M(D`U%Y5Ml_cA&?B1!e=6YIv#M0|HO8su#7UUpgoa=++y2 z4Y0c#d%rm#<#zabcvvJnG@Ft_H_Ir)Y~OGt zK^P(A*wmex;_8zv6E&5~6BqUyyfu6e`7P0BW?*MYs%$h|QBJfptx7x~j(#YEjFYDK3MnK(Jm14wP5oBEzIa5FYDyc_ww813Uh;QL_jkAue z3nGIr2oGW8yjnrhPtCKql__1KI39U+lfPXstb`T5aN!(`JbVY(;SZ%X+cEdA3Ec_R zCq_ifA?c&xH=%Vt1%7w#-N5;or;79yE#=B)OFJp3^>gYY;f9YS$bn5hfduW;CqPOv zw;iONNqnl-S)vM{M25oU9&$tvw3hIk>mFk6TRmanKK#A}olMxM+%xPc492?X(|HoGp`KPli`rXAw%y}$w$#1ys9r&dTDgarPVlFhtcI}hpXAezQMc0b z8bX%lT|Xtt!$XE7NsBch1VGVS2B)Pubb=AO)y?B77Q!`~32)pSPKW?*ksnWZ9UH&R z@tB_9hIFXB{5f80Y(ev`NUFp1$YWqFp*ui#=E`wd*mx)RTRlxd0?;f)jUQ>ZI%)fU zFH=$W0jMhTy}SsizT!8i^8;0We_$Y1m=NW%s9%0}Aa-f%Z468bfa8M<&)4ty+}ok2 zQ%(S3Q$Bwz3|TJpZR=G`$P0CHzpBOAd-f1s+xP}hzFuaQb8F|Zq_Jvkb>tVx>K9hP zlC(|`X2j?_fUU~U!s0(X5}%KNy%Qw-bTikjsQ+=D{L-nfL4{Q*S!4vXECLz|)AyvT zUc&wOK5)KLYX^r?c{D?>s57}L*g@-AR6x=E+feY$%cUdt%j@4)-1R#uf&?t*-T>$0 zN6OzFNl1KR>wOh*iCAv{okM0ZTqg1Lz0vxyj2YLnr4fI|SANAVm2Ej|65mk4y8iRl zQi$jDx38_s`uL0`NrIODAi#{`$hD*)>F#^te9d{NEdL#)ng8eAbfEV582_oYzKU#{ zyF7vpzo`hslobL{7Yk9)he-J`(;2<9v*L`sn(^iTX4TOb;Owx9*Np$h76Fp7J-T_j z<@u8zb_QHj?l{qNr{>fD+(k?+;@@t+K)lnDM1G}pIN32-rNW>rYPtca=q&L^hVjYj z0({pNko|w6S+HMARArpksZx`oR_w{>zK)s>a&foo?~=~)l^TpjwGhvoL0!Ydqf@77wrq%GYIq0$rOaCvrhpt z;DoB_RsW9x*=80r)=7NmeSnsD>rCdc$& zJA)w~qsrhipu`=S>M&uXv@pmA@ z1H+W+&d^JOTuYSfw4PojWd*N2_I_qSWX@H#BRTw$Ec|9^WByWoNq<{?ivVjbRUe-` z)+lXtd$#ZdbJ48a4SqH7eP7SO#NEK=-WaD+rP@cY3}b6QdBJyY>ZmHYu5`8h9*0aQJ=hd7*KL_%#d`T)jKs~~ZG3c>}xKz_j2lV2M z`TrWQLN5^R@*SAAPutrs($FO(TN3@!?vBaM#C;;_iKOz8VX5lbWKxsC&b3UL-BqNr z)KrgsYh@?Cf18$zD>Mh{i2z#uqj{`0D^nQG&cHvG?rWQWqSQ9E!-Wu%;%__lUWgTK zYgT8Z$E-jW`fEn`Kt9^@>;>Dh`|*R8ovHgo_KcWdXNo#7gW*G^0iu&M3TY|ND@x0_c3{Tha-S*~@LLrV=0 zsc9SU&8a`EhY_wI-oUm3jnSJ46Vu&^LLyy>gYpf>^dwGr`#=_D2i)UE2dBNMS8cN^ z)F8KDNMfpm6d%X~M^K*ayN?^fKUSkln)CgTO zdyDHD6$pW@?$h6kN=T4{RhFpmTvoi?oVTYXR8!u5oy}k-%!PU&`Cvq@ND>+w)kIYbRfMcd%rDzLb-|zG^ z&mk>zo-}${s~TLfkcm2hC^c5ZzaFggV*Ej1J*Dddi+ipIt;8G9`vjIPq<&maXIEEO zhlCfW56bfXlOibDYLGQ`&NR)m4-8A)M@4k4e#6K~VRcDwYj1=`#YYvmot!oD%DKMu zDfo+iAuSZjs*#3LJ6#A-9jSq0HfdgikF%k(%7cqDd(Yx;k~aN(Lw!JMu&=skY?(kvfP*Q)qse-P(@jrz*E!DwD-K%*wNbP+6 zNbg6XN>0*J<+s$n-~~x~%N7B}*^W1E=VTj?=?zC(ERG{kw<~B^l&ox}NjWy^R3Nq_ zDko+33MR;g*gsI=GzthFCJt&{Zf?~h`9`)L8l0`_jT`QfU&E9EZGA>%4V4v=GJ`io z6`KR7cpO>7$lzzc`do|?y*xzO#w67ls_O9Hm8Dq%UI z0067ly0MKw>p&MdkZtVPyBy>u9nkbyzQnAre(ge$nwJDBAxMzmNBq23(?~X8`Zen6 zBz}r~wjdyGFX^#HN0*zd^xP%s4sqH~DS-2r&lbSV<^OR0>6Ca%t2h`7JJamY%P3|Z zKm^x6{)Lp411*p(k48584^DMoTJRo+xKsC@CAPJ5$1tlv%ZGk&YPhdHVmo3~ znYL`u2t?+4O^5|0(!KGsV`14v`+D>-x?XG-731L-{_o$ot;iVYPZTX7cWN4qoV-ST z++l-hJIepGYG6E~@G3Ycbm*hb@jT(D2^2Gx9Y-__6(|*9ciKPA-j>3=1|ED(rrHZ~ zrn}vCYWFTTYyl~`HyU2xbNSduf(f$2@2AF?xIhm6LRlI`kUEj|Q9qtY-qHEdz{R?riTk=Zu@v&{ zP(XsPXM*Xom3OsofnfG7_;YmVV#OuEbf@0io}3_6JgX<1MY^3z+Ow0TcegoiWN^EM z7-&4Q^I@5YrM@TT zSf7~K`-m8<{~HeWhhxV`B;k#SW157f6b5vyG~$$dUHu|%>6C+?iDtAH^|h-};3hM2 zriz1~siQa0PH-8V3HXN;Uo%lK_mE_@+{rld&x%VGyU8++n%)Hgw3P+s z71du`Hd-7Mb3fmOP%kDpKI3Vtez8BcHasCJ=B-I8(yiWO5T;`LjbA{RQXcV9-myZb zi7z{_QK`^svPH_l5ewxVKZ@6p>&eI<>GJ^KKD;OnVwy@LR;Z|_d36GSnD%M zs&OVJL;iplfHW~8n`G3FRq*B*uGtC%%CMwk<~#iL;5HI7&-2n`YGfVkiNxW%O+Z_c z3eZ_;9K=gm`2be+kI{H3|1ZADt%4Ty-eZF+tDTvIL;66$TA7GMkcZuyptzNjv6FY6 zUxo0-sUhU>OoA>3EaljIOlOS+gZQZ;`t;-WI`V!t)s7E`U2S_{u!NQuzmvqr0*%TS zde-sAj2MKT>NN=g>SsiBxvrW}a$j^sy;4&ZEHr26TjPtk|G{Q(TQdhv)5WYAcxp^E zzE$ii^3UB&2rpDQvK1E z0RMhUe_Uj`6dAj;96&8~{7!z}^*`QH7I|jnr#{t%g>?|fCsp_KF}~SS(IOFTj(Jg9 zo^i7&m%HDA!j11Zh%=g=FU%Hs#!Y31*el1Kf&8nAgmDujz`#<`150lb-1Kwg#Cbk` zUOZY%GA;i%0IN)!OpdBpGYGOJ8Qr<#K-#7vC%X+4%vGH;iCC#R>WP?5iDc;mt*N(n zH$Ex%nz=d*L_nwm7Y((2H6XGMYBx z(+<;Y3x7DiN*Q>~{1?xs(*eV`)))_;d>zYveR?k0y{4j{2aM(|vm$YOu1M*M^>EJoED;pk~Y$DYKKbtceo40A6zPFZKY~GjJ0)jxHZ2j((9Z(PYeVzDtBR;;pq43YTmT~a{OZ&an9|}MGcir=N?pDxW zCDAj!|5QsqKLIlQU$xNx_a7fD5JoCW%I@hfW4v;WYxfUnFLq40WQAfb);XehoE*^h zUy`V@-Uty`FO6q z4R8VgO^p3MfWGDNGCNGJVBrARvi`s?S?PTfP#n=yTAPS6yT3jBaaV$8kkVg*;on5mA~6KsTyyT{WCsY&THn0q~^g~aT@l0_hXM;zt#n~#pfVc8h5AOcG5hq zd?sn5M$MPH^KsnzB7vjxDs(JpNoGCNn^fWWWx?`e3 z{ulRNfk*dfrW!8q4jFBiHfqnj<0*S{<(+HT=l4C{2Wh&r8rJGxTp{hRZYEe4!}a9Y zG(?*q1^(RrDfdoIL`|bG;i!jodcwWRsqt%~8&U=4-M!wXcN5#I;PE(p^WDUj$ifn( zc~DPDrRTk0g5pnUQQzxb4d8Uo4|snn@dQr!Jg6>@UCZC#dlSJYZ7T>7F$vZ!@3n#y z`?GHBzP8pCIgX$smstHzlIP$?o>2>qjD*YxLaGuZ*(HxdBrGmDV)s^<{HQ-^9zw`$YI8HkhqE|0(c~UR)E{dx zR)@u^d7bMPNi%t^cKmy)>2!T9w1Z#MkU;S29u7@R_8KkBB2WJkes;SE^6l6)yU zQztl+e~t@`m*);V@-{cTB)T1h;@pU3*6C+=G_ia*(diq6^)BGuP3pp43ElAtsi zQox7L%E=GqT)!GvOXIZAyK94--D7+b#*eXXS}1I&GSdDK(9X3d3TL%&SjWr#XZI-= z&jJ-KPtIj`w~yqjIIFvmTRnzeu?oU@(Q0vP1Dzr5oL1_}LeuZyRf+~ag17t=6glO{ zm<}#vPVHu(f*Yg!$aYC4Ce?=D+G`o@&&d|K*T?VHGO%JDzfLwfzjz=3J-Bd_b`a&C5h z@7X~BF6Y7!?xEKb#%+dAuAegYnm>2U%Z{s7@Kl`M)%s38uf91drWjZ&9taGtio%<- z%?ebG^$|QMtNoGRcul}78{lTUeKW-w+)o!nT5}71y@o6Gx>n8ra{`u>zu`9KTvjVc zZD!I|PO&naKGcAblH8S84oL{$pf^;HE5Vx_M5G2d+IF|r?(Yj;y}_AWTqUWeB==Uc ziitQ!3Q`iygOTHog@{3}pLUCXFB>yDG*EqYzc6}uSASl(JDKBt$+syt{ntTrsPl(- ztp`B6Aa9Y!RrXxr4h60XrO;9mq8wdu1|}08t7!~MazbRWNF0x?FZJy^dR7C^i>eAA zCng(H%OxC!&@HDgg?&~JQq&dp_h7ABD*m<9!2b3S=X3+b8P?)k34^n^_r<65#f)Pw zyCwcOf61kZ+w(UKPk;mT_reYlta>?N#;-UvpUQ%Q4%MhY>%z~SzyXp00_E)X-;f7K zq>kP}rX!hsPWKB+l%nf$&qa5-R$!0(J<%(`dMSHYGg=hWlJfVa$!PattL-#gJRbeh z2B@MJPU6=|eu4!qUip_lLZ}78i2X#0as!hnFpTRasP96RKU_C2mcB7KaE!F&` z+wRocOrA2*m=C?`@|UYSY~m$T_v57j-~on&L!OuSvhH^!bKD2_{N*HoH(0O27NBM~ z0?EhKKieCLN7tO?mq_p|EpVlD5c3iz3WD@UJI;5P$*LZ%kx2&O;g@yHxq#NTPeZXg~{%;+hRsXe7bT5(V})Pw1qq~ zCT#)Lbvw-Ldq>zQ9RK-EQ-Q#M)0|~PI}!YOE7sya&My-9N&qxXzk7jp#)TI9vPZ2T0j- zX1GpUF6#TPUAMPt?P9#p>`VODD=vHhzF9z0XxD835=T2JT2Ht4gP(eK{T*1e3n3Le zV>wj$w592OeZc87CRVBjaS5rBKQgY$PB`iAj#@Imtm<-k@gLsLt9wZ>6yP4fk%90e z$p{KOf;>+RjSC!X(ncf3IbsV&wQs<3^$V~2n4x2jI{FB&ohPa_EXNm@mTH6&hea|g zQpp(6He7}=bz*x4y0g15e1g2sT4VCgFKxOwzS-iUp_g4!GNx@g*i=HCJ|N}NNz(*J zjI32IaCK)%udskztA!H3IV66;P$YcS@}xs05r@m07m5r5E;oI=?w<=dh~9rheSsRU zrBq%Y5fixfj#E1|D$DqklAO?KJbsrACGQ5>68@YSZql6t4UBT?FUJHLg<6!>A$*4s zBS_a+>ZwZVXo-yb;!|D%!s=@{V5s^ODV~iV7HumPd$|l)kx8x{Cv_v zcWheia)sa6@KXOK;z@kv^>6Rc{=il+tJae{T8^!aw4L!2N+4jngYL~5&!yEUeGG0x z;+0gB@&3^_g)DYyN=-+Paa~FENmNcnU|Iq@6OVkjO8o|NgMN9tn7z6{Z3`F2FUPpl z(6>KS7Z-=6HtLk~hCjjbGvektLe`T0<+P3oD4G^DL@t{@cg9(XvmE$AR>^MtKowKA zg6Vc>Odu;TbhQ1JZ0Dx*<{$vMseKBy97cF-+oJ;QSWfVsP4Hmr_uQ=3x3XQHjSx~oshxCK!%IhgG?#d1Oz%v|oNNI9 z6vzcwiY&!dI@tx5GF!H!I*l;6Sv&UP_nWMpbnzUS0KJU7TOsVH1}s6@3>C)CVE#^z z|KLoEPcMJ}zM?%VF)1MttwQ`!y#tE2YxpodQ&?4Ym#orWCqYW zawDeHliPx3)m3-J8Y|Mqv45Gxk}%wlD+-$lL%}mhn6$&kB8?!sV6%@gaXHQ3o)37M z#In=56EIXp`8_t`evzq+K0;L-r0tE*B||6QXg&aoa-P?&jm@{ddt#FybHmE*&tER< z-+Sw@*D#zplA`G}tat4o{upcevP*s4wtP1zV9NhZiXDz`NY{-+i^4|0oDgJpx(yV7 zwfdJ~0zzD`*9Dt;I%W$1`*Kh6U%Tu{{0UegsHeZbKk`V7oXR%fi+PIruRHzthhJdq zwtvy~f3|rKf7j$2Ydu-MYuhk{xBeo@9_G{Q;)~oLM|+ zGyS34a$TqDvp&ZQEdKh2ivKmm{7v-bk-uE5^$w8Sn7v0-BEQVC3AbWrgUjMWjGH0= z7D7Iq5W#5i1YLjfRPWa+$Y z_{Y!#xpdL-e=ICNA$yj&a1zI7V-vvoGc<5aeGeQC*YfSk$uf@kzu0^4uqN|uUz{=n zvu%t+i_}p@QR&ia91A##prV4bAQ|aNK&n6pZbwl_h>A!jBUOhIBfTXkBvC0*T7XcL z2uKMS0)Y^c+!xdt_srhsoZoq#``mNyZ}V4@H{t!3^oh(wE_LtJ6 zte`G&im-GR@)kew3u=9RTcu=*&kGtzsqGlxwR zt(=7jNL576RF*8PR2S2lihqOIT( zhu%_df246Q+SvOGu>?Gl0)4`zw)+>~dcS>keJgqS)6mr-3e(mR+A=MO$nVQpY}fB9 zd(hP@KOS|b6hb0Cu+q^4R?gB*&t|m=;noMx?oI=8cP?f;Eb26m&Zye?B36*e% zQo$g%pLYzE-Cu1LQ~zHve+T+Fc~K|hY$VqAxQ7gv0co8t^&9$bKQ6Z*Dr9*$vgV{? zS728lXUc;&d%vHEJW-mRLbC){2aRrba|Y7AzVJjA7M3VaZoD{3yA`^cP(IQx#0bqa z%L04o?KcY{{Ys2PEt`)2`t%9#@_23B(S-3&aq2z- z1IT^V+&+G$_L%dgglmwqqp z(wR>o8?JHT1pB&Um`i!9PQm)JitV8R>-yrD3|s9*t@;?y%GGZ$rc+Q&Eiz;vL}!I@ zy-k*x&hGDKRL6~{e#5Gk*Y?By6FYY+p3S47h#@B)-T`W6*5w`$xhungsGG}c*j!mt z5uHU4_KzF$}ESOErSte z?$8`oV?@A{PAclcOHm)q--5vVNqeSo34k2f)Ow#Bs@n+Z)JoD&k702iNKl^1`Y(Y9@}teV@J|E%y;*Va6Y`bRvS5YtmgWlnwy?m-xd%?IvlKG+1037O+CD9 zbGE+T+rP9i^!?5e%bH>JKqUk3N_zV(CwMIi_=usN$k6i~B0}sl4{tLs&^ffY|1XDD zR)3-+i~f!5)N~?n7%x9xhgv@Cs9o9~b%onObCA#@3@dKzu>1AbWdwKRoqyC4a_gs_ zqtkV6(>|9U0Hp$pt-(xpW?#~zJgN4D>U;bX>#3-}iK-S8RO&*=+lkDfC3qJ8Q&W23 zlBASO=&g#^|4=0VnFfyE_v#OK)nuPIbfP`uqfz`EPImkv zjRyFxC_hT8_Ixz60RC1CWP&z4uATwl@?jZkqX`;r^j}>50YLTiB&+p@3pl@~mFRu< z*~S3oV!h#c%V5!{m17N!ZNCKw&;F$W+bc&x=1O@#aM&}pzo0UAEq1-DPwZh5U>pz5 z{}B=WZZ-cEj+@mJ|90m{PijKcdU8z+O#=+|g$f^Iso+T(PF-3x>{NTUc?E1odij4~ z*>A0|LesY;mfT*L`XhV=FiNSQZba_nhR^Zp#H){;Hy_cZz^0Uk zHAmtG0F>qc@WLOs=Dqkkj@RmE{_BoW0N|Qj06Z(j(24e2K%GFuJwP0~p73U21&dhaZ{j+2tumQ8M`A%iP-d zb$xF7uKS{MbRDecSXceR61M1%p9Ntzf%dsu0os<$#-bX563_AWlLP_bBw-iM=5%=j zIAVlUgp9_VdnP^@; z@uYK&H`F}=-zBrAn?c&feCV9+-Q>9Aa;A&dW z6>BG(`ws#t{^h>5Oo(H(t|TRsA4w6=t2X|1N49zBphRmhP)5W+BvvTUFm*{_S^DS| zr|c?yh)?ZbTy^{A+Ob5fb%y}ft!d*~De)}1ETlJk8|SWhaqA!+rrdY{$~QG3zbx#y zqBS?9#t_Z)Mft@9j#xXBe-kEn^JRgV$}*V3nfu#dOhQ=*-@0t6;L41Hztr7};!uZd zr$!>7OlaoYnb^PB|0;E-rCXb>;8|aPpDTQ_x-8^eAkL@VTyvE~XsXJA5tc?0zDC;v zP^L+HaC))RJ=`%Kq^v zhtm2;yV+V4EITSHQL@dux4F8Xhp8ge%IiniRPXKt$J(}Es5Vg>BvcS)pO_nZ3tyHI z1k!Hams}ka^A`sDIaH-1qt-3MlI6 z&_(P_^|;D@p+%B|tQ-Nr&OZRu{a!yDZA~wYdi^*caJHYg`+P*>^|J}z-ChFIg%e&Rawyw`s|H^(v{ks*N7J&-@v)8THqUW9c z?B_+b)4%XK{1b2>Ej@eM!}qa3yNOH}M~fn-513({v_k1?v|E?bfEXBv`;GS*-9zkL z`fRxg>Y?83fk~=)8R(7#<*;eHzpzO3Qw_B`hvD6Qy(1x+2EKSH8+FDN9#Sf>jaH!k zMuC-1XuHX~0a?%v07BbvK(+c>b2c1M79pA^{ZAawQUJz2ULYMl3YhPfibxwmK3h96 zREcw3hO8F*ID!)n-2}o~%ej$y>yP#T2tfDC1xaF3i6Ky~W|^Qi))dMR5Q?_%@oDXv znZv6357xkw3k@e)_n8iX7r&in$&gu!dhl|@)H#Cj(WJgbVFOU={90tn0=9bHzlV>20lz+0L+d`M-Sgsz`}KQ$DLCx9WQv6w{5N1)tJbXgg-^H(JL6vb zMx-q^2TXza>$Po(8HT>qI+6fSYhy7Dxw=-&`94R6ci<1}3;bX^3AwHN#}N55HciT( zK~aGGu`7N(T#wyYzv#b3e|ZIwRm_-t_t9RrodbP&>DLlp&*s}*~?EJXpr-vRnMf5%s{qBExfg7N(P)xd5b0e>f^Y6n@OvRn8ew;ASW9e6he^ zHd3ClZm>Ej7MqtLX?lh~Px;bYGie)X#MzAha_CxmSv)^>iM+ZrqufQ8jghzA1Y$tD zGXSqiR-bDRxIO-xzir9+wgf@i*Xe3yI-x7HcSlX04C#9ooAGudCHX}hTzUrJbX(1f z@&ovDHil?B86PqRV0zW60CQ3`SWe(;D*dqSwYN*NOZ2&*;XYU4Ex@F1?kF+xX-}7( zTm;K8aDJVP2wGp1OLKqO!pf_O{u9hL(^#Jm{1iOV@H}qlVx2vZp!#y7_j|pg5%)@? ze;zA;_8hUZ7ri6&eh#Q@qJS6bJ)k2)?ZlV)Ymb_HWrGCRpro=iog{OhBzjF2o}5sP zz?_?^5diq5>PO5LyTfpT%8P_00mVG<)c-I5Jq_fg+>z^USZuIsqT;yMor#m}=Ri3C zKTbu^xgjB>hMcTE>vsdgxBDANz200+*ZOe2%AxbyW^V%EZ)zW)AsZO~fdTvoAOaNE z?s4aA5f)#gCo0PVJ8xaWt(BysxJCUouFWey1ExJ6$R`qo=a%PgCeU@%GEID8RzG;n zo^Ob~xh(CFY^;2O)?AA`nZ0&(TU%<>C%o(3BY^3socJ7HdW_ce9I+$y{!cY`U#6*M z0o{zizOaKA^|E9Qw}B1%Mo7>_Eq$MCP>qPw*&jjY5zK}ora#C+$9)}xv@2F93Eq=X zH|RN6diEtkK(kIkL=E8jZ=?sDOK(|jqz4$#r3IOPmLAv*WaJli$;khdD3^2SXuU*! zR*A7~-#)JbX#@~*D#^SXQFAj5QWb1Mhq>iB^-%=ZWY{BWGQ-3J3Z{KkM&=0G-FVfv1O*epBA(SwJt)GFn)AdhB;7_8$QaZve;%yM9z{Z~eKrH>P3G zUArhQ%%gwBU-Wy=#J?YpI5TD31kiB&;hwMKZ)eb-(nXcz#6v&{#FVkS+!rqXUouSp zfv#IeJlB=<$@L7^#|{lKeG@?cD(Jd{*5fKh*u#=P>r>$J2DvU6756VL-(6%(G;QgB z7M9X^&9j{NDfRzr=ek)0Km-H7tT+x3T2T!R9;R%h3m18glRsLhFI@UupRsP!@=vXC zuPEKTZjo`b1i@ zxqq+BzgGq*7yeJdx&K6^dO3HAOgjO8dYi@jUvFHKv5BhL&(&Z#4c5e>#KpX{KivUH zn1~>yhaUH*R$>9BWP!9;Jdg_eSecmvw0$g`Iu-7Z<1m2)F95fFEbF`+0ew(4#a5s4;7#=+@`29}}U{sqxn5 zEbgzZ0H#IuQyuBAk2S&kU6U7pe;sz>Q$6Wk`viua{E+P?U|^q1M!yDN(RhpRJ`HEn zm9P4{$Eng{VmY5je%(rJc4~nMe9PhUNDirqal?2(+wVcmPt~Recgl#x)c@lEZcE2o zd&I?xiM{%Ltj{Hzb9KQs0Mq}G&qI1A?&X-K4m6>L?fO*EDz_k}e+Ry%Cqous!J{Ag z9Yy(XJNZJp(t0k}Z2GV|%q?!$yFU-n{+9~f+Gu?j01w%lfc`7teaC6*T6=L5^p}}3@8lv3?tg#h?{?b_ z7PO3`Q~x~vS0w=T*Vq+BUgK*yJfl}$dH5w3XyUc_yz85;h=EELVENI1+fFt#?CMpP z4UxC~%Tl@AV;-%)y9+m3HfC}LS|{i^_Lps->>}S_Q<8r0sJhcQbE|SR-XfOa3fleIbW9h@dq5e z?5_hm{yOg_=%;91+q-|KxZI=PDW82_)BV@)d5&+-Q*5%SZP5P9Ztp56IY8MDvBUpm z)&BPi@8jaOKkvd{&JBGqbI$!f5mgY;ODxY(}%z_CHb)m&a6LfAW-7MwbD{nyj8c7w&0 z2%bl*250PrtymlQ(l5|ZumKK%{s7#y{`(c$M`d|+w`J0p>eZX#=)<)r?>i=MtoV|cJh@~g068Rj@Z4hB^1 zC^CSDUYz>dr!BDumpz@O$8Bc-15}HB!(cZRd!p&1_L6i&D+g;YIJj^WOYHwffwZ=2 z9P(N|t#x5`6Z*x?tto`g{V%Y1hL%1>L4Ph;b5zAQcD)E_zvD+89Y0kpukXpb=nlz^ z>!XiNk`Nx^Apye77pXGUlM%)w#r1zMwX_2Kny3I)NBrr>z5vw$@T`_;k}>|56&^iv z)Kt0xr92w5Si_F4KQ!isOBQe~4d(!^{F`aY&Ow!M@KVp=kfpJ{XesFe^+Xsc4jP;R z5qddMM}-J@{tv?@`;~j(%4l$)Q!L&k(zU!&aI{1{5l1>ZSc4L{y7ydcAY0&~-Vj#C z1IxxCtNyX$rQR7wLzW~9nX#-Ib`!?8dcs(GPV}Koy*im)!!EO3Gzaeg6yfGvQLrS~ zrKZ9*SMwKuF_THKV&=+{3^UoAPmcNU4$A(SnV6w|6veRMCJAh+ZHPM{Sq!yr*gxD7qn>ON6n;YA<`l zdRm~i*)cj{>?4`N83c8q&th(czF;$_n54D>TYHPuqg2zd+>9M8!*v3&-?tfP-4qb} zV!21QZMkCVH6CBWNRux{=p+&kW zP51at(IeCi=qDc?eRbKF(uZ=St_8#*VEj==ToPDSbY;ATnTA>Rb8@d-XmlagCNW=P z7OwDk^fM3NFPaZ6MPxo5QzTSvuXg>BiA5z&sy7u7Iw@-ubNvNhpGDd_C)g*wrY{KF zu1}0P$>dKohj40ovf6?ItT?}}WwEAedZeYBy1haI57vJpzO~pyx?c3*Ea$;cUF>6I zPkad7k>UNkuQ*Xl&h^@B{Ms&J)!WRKcIse_acmZ%RaQW*>;p*d6TDx?rL|ToKKXGu zFq7Z2#uA-x2AS*~jYbHU<{n5tK%AWSy`p@*Z?&Ri{|!}NgU1k0Ncobd(X#(af|xaI4#Wii;X==9 z0~{V&Kx;3zkyK6oTFfi+lRN0Oy2BiJ41#Q#*6z80INWH2A1RciN|RB344*doU=y3s z7mup9KFbh>f!?U?_F9{?%dDvjpbybV#KN!>55tUG^C7b!vB7HdS>%A6qe-rGS9zVI zYI5PJ9=TDk-R(FTPw<1ym%ZGZv#CKf2RPu3H%HlrSDjMhIj? zmY|{Hz`@aL$*%K9i?t-y=b`E$7+#xB*-5@Ux*v{Pytc%q^CG0j#q$CAqraXqtDv3W zPd?6K#aa-TX2Pmo$uMK|Do65D{hI)Ra9qpVao`DZhcIKH*LR%~?vwkS9_K2#gct0p zw*GL+YNfe`8ROX04=NS5Wa({-8Tpal%+7l+QumKv#;lB^8el7i`a(O-j!lPY@F?Tl zW3)?qbH6rtmMZa+O@FRTfJ_#tb}5Tp<|Z`C<$q|^8+$=1OSVyU!>#xS@sCH}e`d^1 z)$E>0ocw;)JL^LKh2Hv(E{}PJ)=A`EvK(QR>eG>cVRU=?PT_giB)Ob+Yd7c)3kY`$2hH#q)`&cG3X?1xK@X_^n4gYpQM`3 z&Oed=MryPr9Fhc+F~XnS;aVP5WEZM#;XyvIVzai&2w5_AHmYuDW4;V$U-1{IMAYO^ z%AP36yoocYF0=1;tx1k%Vb1)b%ri7x{mY7tGsf10y-~s!2s_cy7WKX%|2I)2a$2h@ zcm>ut1OmZrVtHxcej_6@k1dU0UQ5)I{?0%>iY#OY@4_aYSBy;*js~e*eXzni(ZTT8 zjW15H91Z_Td$o0SrH@2X!YlGkUIIzP32{JKcD5&TFcisrJlslSz5IB-!BxNW;B6s};@k-X6W8V!@CY9F!}5nn ztZ{kc4x&5z`}sS$*9m*eQI$I$D>;V+y$9?FImBiuf8|Pua`7lh$??8$WC4ko_O=8v zyEnkpyd7_jJC~tX|71k;Z9TR%0);3sQlI>Ckj8p>Oc791xzUCAr?&uR`Cq9R{ojYn zrH}Yf*^?0YNp)~Tixn^&V4NqKfdQwa^Z~{7&9?&`hV&*xOU2Nsi~hnqGf|%#b4`N* zG2QLZxJRllyT?Y*>_{GuED$sqcS8E#hU-lA19Yt2cJ2sy)hlsB(K9^4UYyMyCND+> zaKUEjd7+3~9qZ}jrQ*SflV#hJA1TIk`TkAJOX5@Dte=xz=~H=i=x-|TS>$8aTKrd+ zREvDSLO+WVs@X-c(wDW$d=I`IpHv zXcbR+(anT%PLpeV-y^K=B!OVex*@;0Vkr$$VGcJ(oeqswT5Ov7t#++?;+(p&Gfoa* zT+_Cgx$$(#qiUjv`}D0*u&290m(oETs_R_H%)~@xM~$Z%b$Og>@G_d8jBP^<54n%3 zI+K6G&`qQn2p44FmM!kGn#Re{!=V`+HIz%JoMF$Ihlb1>TX`0l43dAxIYP*Z8EgAC z<8qZ9H#PcKZlJm(>@D<)Gg1czyec&&DFBuy%R?eCcotX;PD8<86$4OLT%2+0* zhQh<#e#M)wT+0n2@_ws*Tg{~%GxTy-o5aIebXC*VjHB7_A4A5qkV+^`R-Wry%?1dbI5!1B(pS}TCVODFL< zhd~EJBLW|zCc0|ZlC!+YSIe%BcbK*&@(eS(O)I5RutYPHq41O0My5JouuYRs=;YJ4 z3y7u~PtH_jxxlCOwc@CfWFT0zqK#@W7VS$drjID4iI#>a^*-vTD?xttj#O)aAP%Br z*kz)St6nownP(7h4Hf349*FbnwTdtUaOQzd%aDna z%FHKFTfX+Cc^R>0qfZpf&0$?Ec@WOkW1H$vnInptWdk5(1ktL~I__fVW7F=M#i81X z6UtC1({#X%@e`JwKwrWwHA7Z7<>~k}11)AVN7NQ(A^)v+hM(G23HOh8D5b(=29cOZ} zZ9hP$^V59H6=gm-FUhp^WN`=F4KaySA?oD=oP{w%uFKE5h-fZvVE%E+*g}?RE2SZm zp(dy>ip2rS$nul+EJ`7Kg-)iW93x#4-EpLnP2uRRip8UFy$;I{-61U$Wp8rA1Z8NT zP<(c~1m(?}SoZ22sRSxW-_{esKN#(l@FQQ~OU+}_FePw?ifOhHzy)Uqd9o8O7-ADX zNFojLD-ke&Uy+9Pv%LRvCBQm@RHdID`Yy1eEM#W#{s~?*!zRan}O$h zenLjQET&An3fy>GhN44Ma^R`=q&V2@QJ07aNT9yxuwo6Ibk@~bshv1J@H+@=~x7?NuPo-kQbCOv!W(Zcj&rX$M&cUOdHf;o?-0 z6^z4AUx}@LGc!|}{FLeMY2Y-Ank01tGTM>FEHcb;?rFE4%IqI{`&$Af4pwKZD}42e z59p*tBK9$)WuzORpB;=kk(~km@xJQEou)zv=J2K;1gh0=@pHgqNm#kADAStkEKkT_ z+IBD)msm1azuzXcno&#cEw?$rC!>kPWapjYV)k~QLp04{DfHUap&HMKSmqV!Nky6F zk=PS6PhVX^hMbw%NmE8N#=gJ!O5UH{Fxk~LjpdMIn7}%+$je-p&0O(gWO~M)TZJAejHEJt!CmS zeir1fp1^9$259cWwLwfu?X1QC5tzKwr^$=Oh}A?&h@~6-@g8t3yt+Q)LbKvPF;V7f3Aqf1_gmjRKI!}Dd1b<@;Cp`uWogLfZ?Xt5 zv3+?^r$zk1ue+O1!Xz$A87fL{kxFVBU=TrzC+B~?O62K|I29+JD^C1wDaFo)MwTQv zG=+D@j98qL8>L+t1z6?d_se=6{ONBWd0qbf`rqsDZyESMAOkV)#BHa;zOI)CYc1dP zbXv38XyiOg){xPUw>$i?>+5WWnB?)}|Bt*L$dv}AaAr>9=}_dwCsnYOcgK5X0z+1_ zIxgoMxRux;$A8{p3b8a-pIM6Tp(}b{hVX4TPkf6aqTD~M*_RdwdYs4J1y6=L`F3%K z3YQ)>naa0xKvg}^(M<{!8kSb@>Hb`LVI8fg6y9PG3Os3OUy)4Fla`Rng=$NKL1={; z``zDs+Ghqs3_AL=b}1byNLhLaaYaA!F}t0xsRV&FR+GWCtKqXj0ZkE?FIrFIN_}@y z#$Iy;3X(}FQB-h|@)W z^Q%)^tijuh^?LG!PuZa8xV<-|mQ7zyJZOLU;_cexl8cqat8>1;{rG8f2S%hob_(9s z;d7)Ai@xb4mrQs>pNz4s#=Cr(8x95AcW4=KVukfqD-WB(tsGF9FpB)6ew-{x>7r}H{0 z`>gB=e_!+lG3J{vZI1wld?OXZeFYEosBE8aLq0@l7f;4i+5SXDJnLU}{ z9pz)bS+N?YcSDj)Wl%;Ox(}66rslVhU%i{sZg=6oj+)_VH$S(#IxFgoH920uok#-m zmrsuXi%(-&()U@LrWK{t>-(~j5@kQn*H@g97*yqMNBQLn8}*bcsx`&8l@bXV%<=uUv!@&ozHu~Ci2RcEW;M^MN5 zBn`rSth3zf;26LPp2Q6?ussr)0fTfxK05k z#jP!iHH_bTKMCeR zr{wuKHO07P@RM(uBzGm2?()sgf~jQ}e`js+_)y)GbV00Nkpdk)o^NOZPLxep%}1>b z*Tym@PM1f!>VeAOYb!gRt zSydwpFraW)Df1r5$~>+m3aLwpfzMzMms$F;+S%I`#79@o*BkA5#j&s|+hKLH2U2#G` zUER@u`!gz62`kPuV+%tM-h++4(@aLz*f9u-Cc>CZi)m3ZW|mFDinv&joQOjb@yjn` z6UBfO4bXf1q-m@EAdu@X<=Sf4uT)^iY#Cg8lLJtziSu|ehSXtq@#hE4Q?;=T$mzoB zeFegw_mvO(eI-5WA<&kwvzWPUd&*t7R91^@2+7t0v#>AZ(qk{@<-6ly7pxg}w-?nD z3q`NK`EgFk>s`M6U9uJisvwDG<2-M<)z~Dpl@v2E;OSDs7`u3b(eJH< zk7^f9O~<-$r0prAQRVV`8ilVhv8~jI0Z&H#Ox@vlw&vtm1%HeF-AT`!q(ap4g^1i@ zi|G!}x)1822L{*AM$;C6Il^Zk;i)laKc9otUCqaGM4?M=1A7w=cRb1%!J!XduoK>j zf;Mj7HMH`6$*f1@p1u0c)k}R-p4k-*N&ON>r%%1 zzCl;gj1B?x-9k?6)A?-)W36_k3sVnuspF1W7Y|(wnI}b=dcLnKYsghcg3B-fH~c8W z^A|ovGcCeEA|E{B0_z#CKv}B+1bv?P#Z~N?hfbxIG0Y=9qR>scU1!gTz7^3hDEWMk zOKPg0hZqC$vJy`i!L-%rQXBk@4ztbont#3qc_Rj_qi}{c#)V|hu>q>*&tj_=6o044 z_qfyHCd(0liq+Q7n}xh6xg~1we{KMFkK)(fXsVbZoy3@Ld2X-WXuU&{%(6=TJbu>2 zcmw&Qr6re@?yD@Aonk(>&ky@V1~F}~<6~D}Gm3p=m}JlkjcI!xq&?y+Sf+k>9;{}z zh0-6X=^X_jv|DdMQ<7SEv};iIbmWbOHmeB00c@~i$W_r}Op|p1e@l%)YX1FhfQo;6 zNZUOf`@*LfIWB>IIcd4(zQ^_1Z#thYcje zJz~eQs3`{No~Bl+NgYfNvmD29BaqX4nQ?WQPV3lZtix)Wk23W)dOmmLjCNHo>Pi^_ zX$+>kd41ib4clO3tyWRB7PWM?ruy4A@4ZWN+>(1sXI-^bRMA4=RgmcIanJcGm)sp4 zFO5AR*wu_j+6NyMqPhvm?RGkNdNj;tkF*oA+aQ3jb}`Oj(ia*AA$U%?Fed2&_W)9F z5>@G$o_@&6ek)VOln6 zg9Th})Rx&9tU!-8e)UDfFm5})pA;otj~Pk9l*IdTg6(MtD~){6y4~G zF_d%i?fZJedk~X9^LH_gvf*H>`+=bbtI>w{_BxcEjg4Iv%SS5xc1vAv^6ifr&3hd@ zdZMk&wpXZ^QU*0Kcf|eO2tmtoXRb$A72Q5Q6JuoO95jy?U4N7gu2Bk9%+xp@u}wcZ zhw))%^J#umnZez+aB*A4F?2HP`lDP|JnJ{-tY@K9#;nQShg5D{ac5X=q|(LHLDXdE zBsh{)5w)%8_C_T5@ZROGK>qc=n%>Xjj(DqyErPDHpN0|p!{L{0`oLDXnB?cVh|T(a zG&RH!3uk+>RENCyv;ouXf~h0)+#-IL)P2|>*z5Z}ob8~)n&-W()vppzD^c2IijwFT z>7pNiw}|g4qjLrl8oj7@WQx(blX^6AK*XFAVTB!Q|8rpckQTuC+DTq{BkTvqK7oEk zSzyNw7B`@qU63(BlVbTLo~+4!o;%;~R!>~MURYlf@+(`>E5F8c?4Vy2ulXR?J*FvU zj>F4v`ANgG>l?IWOUPR3_Kz!n?o6gwJ_%*!)7cwSewEO4<8T%--yjBV9TUfD8Gl&y zHHbB1r%g1-PMrtHeOI2_FMf>qH%HpX=T8K(?jl#%R?D38$aN+L6wA-@U&n|N{{eL!5 z7_G;NT|{lcZkc@0>4io{?z*uo-!|0J?AK_PEn0haq;Yj2KTbxy&#yf<1M|9^HU4{C zhLA-208hfa3d*%otvaD)&^@~a^@A;x+rmrV8}@RuerAz3fg7B|2Ldo}w%;;$;ZulZ zg#ldZcQ+i%kFYuvl`l5GysV9oWL(x#iv^zSox8dT4=dnF~oYxwn$+KGu z1bTXuL!Pw3tSh%)Rm@oi&4`W8+ldOo-H#T>SQ4#PdB*tJ3)s2haPwcU1IV8nWMVNt_a(y*70U%E#%D z2iJan3xY+CE`OshRg{+Zht4+6RnNeTUw_e$^g1~gf zD$MFWI0O4ab%xl&Er(*s?y!By#mw0a!*czO1~1MN-UFRYsIofvUQqwYP%GpBCfHnG zrR73@w~a);Ve>=!g!k-0)U1db>p3X|fsRq90GpG6t!mVdle*4j+;tt z>Nb!sHV?3Y&jdS4VX2o5zO4gdN{dl9jc@J-k**npTs8<$#~&@D-%NN$j79|Z!HVnW zE1RNbJHrp%?VQTo#P>hBv#MprGd+z6O!G z{x)+1woTK$Er42zd#n_QtIVNoZ7Z0jt56@?NAVc!$GwdcqvG_DrYG z8MwZ^HU)ZaQJTe2&X+kqs-~sFoD;X@e}fiE>QUZrX5oj`yVYkOp9em=Ht{?@H)qPS z7g@|ALu)Vv$Hr|vwZGb*Vj%hnh#p1{qDjNhuWa%3CM(5~3m(PZZ03a0PGEO!eW2s* zVOzG&Vz0jF+zx7^a{O^~2P8CN5Yg4A@zL^wmX@8zwH;*0%~!9c2dQ6u4MhQXVA*z% z7^@(o7M0eN~1;eC5h~z@6~0n2hN| zCa^rHbJz`AQM+L)HgCRLT(u`N@SJuB0)O?jp#Car$tE+%Nk`mRgBZdgw*t$J&pG<}wHpIx_RoG*BTI2`tHX5yU3`=RK;q=>g>QUH6-xX6s&+YnnyneHf579f! zG{{v**M(0S3;+&LXeG>|Z2EUIA}0;{K;HBFx5anw{{QrvT?xv#b-hLt`fF{Dp7!S; zMgpA>9%c}Lz(gQO>UN4ywjY+olA^Aw<4DA)A>g08p&ymAtbWHDz5>b2{PZ=bP@_3& z=yy-uBi`VEa@8Q9C42MZ{B;x@a?ype0M==qA#ruX?=6J{Sdpw!621a%!5X?;I>As( z-e!vu1>55;Bt=`p+%VlKl9(rY9Y$OT-j3B&=<@Q~ji(x%GP)ja?+qEia%5p%ud#bF zT-%f1EqQpNuVa<@ZZU}cQk+#INp$7tU^J7rG#G2Ytq8eT&RF-y19Mw4#%<%-T|uSq z@DOB7V?$G?{dl-*QmTxzGT+_p=A&6NmK1b>TGhzv%jYs*ocCy8lvm`O&$`60&$)mp zTwYlAM(*~39B0%>qOT%4tEQW{Z|(D0T)@ytcW}zdvu(>Z+j4$i1Xw)5LS0sR6)LK6 z`t`92bsa_W+jE~Ic2RUt?MNl;M9znG@7EX-3Ey+$jOJ>yuyj^9-%fSydyTW`4>VZ5 zJV*pn;#>m1+A3WPePp7@loY&1d1+6a(Oe^P>xXwYyLjAmc2Zs5N?shOO2-9l*6)g= zD?p=bLmCxHr0-@pDFZh3)A!SIUM?F0VB=n{WY)o&Nw2~Dozw$=e)l_Y_H z4;`bH5H5Q^%%5#uJX4yJk^EM2N(F*Wne1~5XTckKq)vob^;%luQz<-v#9b9J&ex*| z!?x+sj_SmCFaVcM(%|-B@aU;_ZPUTP#WL5PIJ1#{8~tzjks&G;pW|BqY}c5-J-=lu zVEgQulW*yYd0e(Dvr|+p290@HG%+Iu9V;tz5kxhGP*K&Ro!{?LMHn>cs+y+yzmQBk zKRoxHSLHNA{Th{XTDD_VE_XC;C?8IK(y^~VN^|U8hHEL(9dOoo?{Ah1s(K%lSSy{0 zdS$cw+)N0qIxCT!%zYSn(ziFHQFtpI;(!m_tPd#J>zqKseathzd#u=#ag8x52@b!j zbQQp(3pKE*1#@L^v!JW5d;bPLpseLv1oiVmZLv48^RlO`X=;TmV7paCk_N&E>~@fU zLF=)Qi?fUM6Mnayp%<%kn%{%!2uh*QNWjTn<+^a|%j>PdvRb%z8v5I|P}voj?VvIX zm1E&Sn<1N6;drt9n6GRfl&iuj_Jn7O<*~ zwKJFC316+L~$VsvZYZw=;b04c{n|1s5WJIJm zMOI(mXvj6|;uy86ylU6niIBnKO;fqs5;cF%k|>%rgKj=rUI^EW09^=%djb&2sBkf0bC2<%j|Id3{Y9 z#8Q0cOjLoaHvPS_G6hQv&9hO{T9K{!<~+%pRJ_*jko!jFnyZ?1V-C$&AG4rJx?Y9E=8;<(L>ZR(y3mSDrF9KLq!T+i(d!adhOcNh z?W9hex1Lz7$MU08k~6A0Y>{y>hSOlPA&;U{^SP5lU5cTkTD3{i>$;|SH(w~zr$WZd zJeg}oiZq#fZGwGuXR~PyHXImAV{k#B+k|P!6}vZDAwqiO*u)^A9BJ*b>OD3b?M@p_ znjJ#fbmtGHEC3;@rL4vP4GFl5I~|ZS22Jw`zGGwftocCe3AK9l0BMf$F#R-s{#p^D zz4h6$ZGBD63Eb1Zs{Va=d{zNo9@s#frh6^SJFsLGCHljTAM>2L-|^SwW;$#jh5dbAIZEqKHc+v z?lICgziyNWx+01m@>k;+9LphCMz~ku5Z%DOF9kX?R}s1tf;X@D*rw5HIK1^)ptN$% zaR@)!9z8ogTmusv#-4*@uaeIP3D;b#`0W(~lR-0z#nJa-?nPXI@B6kXxF9B`Ay=G| zVqMIowO4eP5O|CdLh!?eA*V^@wh#da;{uvWBHp=PTOV$HA2NMuZ4BTQ3sJw?5YQ>K$suByhLttes_B)z|VD4hB#9lopYo zmJ(>WuZC{TwBdtDxgWZSK?I8_T>$b~w!C3U&EM4K>^@|wZfjHrniMd-}SLv)V}22e$z%|LhHhVz{w zr5nuW`CIkROb++rRFdl;L$ljTBc_5kT`ITEwM>i*?W9>5$|{0sczpR}*K;(czuit@ zcwmUgun1r95FKp_w_{OaM;@iGUM}y=&;5ICSk0o|=nGnzUa~G^SgmleVnVf+Mj@oK zSf(iANJv_6G-vX>g<%v*s|{ zlNNcoJiKfu$WzO5aTx}Y#xUIl*LZUjE~cxCwRVC4=^ z7)rF8LGN^+^OJETZs4Wj11#L@#a!6pCiFaKHTq%P*kXfk2|v=VeA2f#JWjdnH8Klq zDIc1)aOcp}^4ZmGZG-u0C8QwusM;`{jqL7K&nYjN@U1X7jwlXn3Zq{DkhC;hRRgjJ z*Lp>Pb2Bdia6zqnP%FLKCr>E8YZEPLW-Nvd-rp*_Dj1zn^@?Ssn#Ekwa#X>R$sIuX zfL?A#9HHq+tTpLiF6NpfCI!j;*}A$s2Xl?4g1!zJev#5lG+WVgh8hZrKlC;dG9yb1 z-qu{;%>k`1V(~>iJ5S;1hEtPcoS#fE#~GvP1C@5VN{(9Lg#X*amoX*?r z6XbN|0bz^qjbk|_2V!iiXTKI?$Tt@>SuPCTk)B>0>EMM$WU_+o0_>V<#-{mpN|Dba z%oP;DEQ3Q2K;JCRqB|Iu6RPXf0kP_w%G;EqPhKtJte#}TIYjjy=7iov{j)TWDN7K9 z#y3%`8qVezj+(1ySr1yLV2A@ZuM=!h?F5`@IVnu1XnFU^@rC0`g`s&P1LpPN#+^m< zN^E-u5tK#0&wNf-LfW+LlZ&Z(9WmgC>yav}en0ESAPSSohJ>I8EDyj3>mD*oamtVS|brKPYZbsB<;v(vwM#dH3Y&zy577)j6Kqu)8UfiKtIlG zR5Ddso}Sl=_|2pUZrMLXqgYj0lcFy^@i@rxf!nw*^8$64;tg^kk1rRiO!v<&!kwdjL{s!Y}3Uiy1WLEl=Np zm|ia5C+r1r>1nptL>5;eV+Uy<~ZXp$+?&Xdxajy0b%)denbbEX!uCv(5Au^hH z*Ba*|(@&IKdeh)sK|-0L!VNlwZ9VKRkF=P94&JOqv){>>t~gn5-@2Y3k;uaSkRgq4 z-w*6yc&2r3>naxvx4=6a`d@TCa2cKVXjd~Gxz?yRcADlUx*ZYPX;~F>SE3@>p@?|B zc9lm?pQ_(8XraXL={}Y@{^Ud1tD2PR;wiuS`PSi`&F?q4tv-1~)r~_;uKpa@J=CbS zl$v}O{T zCMR>5w^A%Db4hF= z8aFRQ3QcfIJ00{e=yvIUEePyMzt%YL^WyRT!v-tOV@(b|gP|l(OftUCyhEv_^SJsa zFg7MzAA|#rLW6D-5{1wp8cn=08d|$#6>5`CzaQA|lv&X+LjGVNq;K zH<>f6T3ah#Q)pN2y(?GJ{K-Go7Ox^leVjg@M8!hk8Nz8~Mw&O7+&6=VN*8foO#cQp z(&8VX$;FqR{Cw_ed%NKIj(mqo{^q^i7s=qv8O&mBs@}KcUmHsC0)#|d&@RBT z*v4zYy2_>tguyoWX4n-qGQ$%vDsP@jBq>uFy?)FR7;qcb(<18HINK3GbwwX9sFfvJ z*hGBD{o4`Cwyx?F9o%k}51Z(xkF`#lnHQ!XR?AOHXO%nM_WqpSMyai}f@ZnPNwYF$ zH08Do21UK6Rb;ap4w)Agqp7J#mdTWjt6}JylzVYXPsvTJ3>TJ_whs!&QFyG`s;^qa z4_pHItu03#gxZf~;llZ6=7n_lG=NSB$k+|T;ddv>>wDJKydi3|qM0vUVY%)4FI}?< zEm-hFnpej=_DE$%0susiix-GR+XZ^w&zm=#%_Iu52w=AuYtVat}Y${(zB*Y%Ly zRY7SRHF}S&X`K5NE?D0Q5hQ;Qa{4VfkF#;BL-2(SYmIE|p7Z>t+Y=ya4wC$NNg%YI zS1oVy8fl^(APVA#4(G_F!s4#?&5)h0{O^Qb@On~UnWWITRO<4EH7M1A!I=cNi;bT; zcDiQ73(W!<&PU|G7SwSdXKNI_`}2p?^WB4%{I^!A^QSCw;5%;E%&P9z z*oy)z64PQuU?Z=`vB9&-x4$YF4d0<_Zs-*@9p992r#btzP8SgCtvV)+I#gA(z=Tx8 z`qD?LF;mJ^S5`P7A@QERn90hJFAmF&W6x)rqf(CNIvn{*EsE~$e0!1vGXPZ6a0 zNGBF!;CDj8r#8-hip)PL*8O{@y6fM3$Jutrsq3k=FgLYX_d}9P&RkjTam>=oKJ}i_ zEVJb>;j>(F0R@RjaXu!ga(eVG#|i5vE5M?}V)ffe$*=hs>H(A3M#D9@X^G;v-uCDT zNVoPCwHr1^JHuzrbGk8NYXvJSfsQ^}9FG>ZUAhYeTkbJr60XBeL?;Uv0}APY8yL=? z$P$kHd276R%=&dDmrl0noDfN4x^fKbdtc|P&`K_*C2r17L7I)pG|3vcp0EZ;<(g)6 zY=72LxcKGcnSI(9{?LnZvipe{xZ&MzaZ3XWb!#rljkckGh0T4F>|{H!%gI+9^ZLV> zbTP~W0D!7=Z=P+RiP_lpa)Vq|s)(9I+O?qav}bv=twt)=oL@0uVX0Bd?5n>>Q*QB6 z1FR$2Xv)R{cRX$TIrSJ|ZMslqK9vw%jeqf`_rbt=9)2gmC8f(t1hW@UmX`ape~1Et z?9P1V*YaFqb5NcZBowYOSlJWYwCcd5l;m8`aY@vxDzI4F)C8y{JWs|;g$qhgrCV8v zUz^x)bOgYArW=7?h+H4U7T~wdN1(cihPP~QbPn#`stayMMJI1TLBn=)us72Yq@qaG zwcsVQuxmV)cMM>=$5+R;Xq>$b>9?*87fs`xHstLM+4*Ly%D;lD8LtHkJ{=@A>Z~-I zKOGNQ-f)L`A){c_EL|@C4w@>MooXrWaTEntpro%% zC*ze7KcG7uQ|%}NXM8%r*9oe~+Zt=$@aW7#F0G7F3_tsRqHL;RlHcxG7y zv1z58MP%=j8%nW&N~K+D#QyZW|3_}9#{W=#PHFgD8ut5o*5E@)=k8FSCm<>9#4?ZJ z-EpDMc>vlCq#pv>hd4p_Yr<~X#()y%j^z&^(C9KNW7o0L2i<%2{|^8rXC?FjnmV$o zQw0H>`ak=GtV+&6q~g^{yH^!SpkuIq7)#LoRVgIMcV=-(G2DNh5L&5UUVbD1?oanS H{mcIV6&}dP literal 46806 zcmb??Wmwd~_b-ZwlqiA-h^R<+cPQQ6u_CN=cP>hIx3F|A-Hk|hcf-=KbS$uN7ybS2 zb6@;l+~+=bUhOkGGvApxXU?4Ye9j3{l$XH4B*sKRLBWxd6jeq+dCH7}g4*`%F|ua) z)S&_ek` z0+$PX>oW-3p?X>k4UI?0FCz=J7{$LIC}=Mw|J4aAJwZ-`JiPPL2cl3&7w@~OPei;fAmM;Ivi z@+Xbd6cigL8~qPEB#a~c-kTQEbDmnbf3UF?A&8aF^Xr!xPve9-WRjDU^N*baZ>o5f zluToElC9bbWfXwJ%F60QgVAH_VQyTcKTxbh#YzqbE+Q)`D$J&eX8o`TpD41`xlgDR zsA{A34{?1GI-nKMDdyyZkBR+A$@4;72RtYJCP;(qzC!u&g|}F&r>C)@wY9aK*kXgD zBX4Tc3865~%Bs?Gmv~&9V{!6RhJLf^xwqBYmWL~eNl71UVtPtR`S=)P*q@j#QFFhfCNEasgNov&rignT^e~bAUBxB~(X+%RO`m9sgMC$69TE}}Iy%q9 z%oyuv`Gk~&l;YmPq|>8#Pfv|=EFt*nz|^d%J&tElQE;~Wmsshn6GDAZvTST#!Dr~4L~i&`02Pett!>cgT}o@ydBZNA{3xG4C1M@dTuOJu>&IB#fZsz=DR zc0#zn5M7xyxKAMqb1d(%>k0lWlk!a1rQeR(`Ip|?H6mMnZ zL?!aHDKhi%ii(R54oD}=Zp@R2kP5x_TIpPvR>D{|`Xo4X;uUf%&H0(xS)kGP-FE7B zJ6AL0j^m6Mk`NN1(j%B#EiBWr{tk)q+mThc+}TDoVfPI%oSrg^E=dY#Y~E3vzWXEg zgCZ@bvO;l9fB#?{6DupLXjGJ2J+IzowK=Vh4nn-XewK&F@0AQuFlt$-)ndbBfr`YN zmiijYd8f?*qQ56FU;g>?=hf!ZEgo$F&F->eu6 z1_Sn{iua~VnS`J$EG(cqc%FP-%m*b8kJ~762~m#9+S=CpyPJFX78s&Y`jlAJ{5{az zeDCm(33q&Sv>ezUe*)KYUZfz@ud`V#`fOoQ^mmI>B<}v`=ZXad1!WR*^YJ;u7T`z& z5I-&2MUj-0bh3MRxeVfR8n1uJ$w^DZ5JYQhV>2+=KS=;(O_@v;5m7W$Tfr zo9zU#1_C_~#otEC;ZpjStoR+(BYrTy?vfR1!p}J14~Q24b434lM%P` z-D>0EGzOiTZ3kXg3TnerDAXuPmXzBj-LO0Se*O z>&F{B>=@2CJBG!|jfkyIbBs~fxqWvxe#7FL2n4K|pDs=!Vp_F&w@z?Gr9_&q?6WS| zAXpXiHRX7I9w2q}H`hw{p`hFTPOUoqRoQfm;YRe~_a^P)q}a;JQv%fW{<=DDxMri1 zB@sdI@1D`o(XKA>yhz2EBRjMRHUJ7}PEPIT+XnKA<({(+*RqQlnv764NIH)RN(k^y7n5#bN+pb;qis|3zRQmGeXL+|U zaCUBX@oP9Gm&WYe#>D>Yp!KK72Ek9mx$!!wP9nuc=hd@I8yi!ms<7Ffxkn5e(Qd_! zrl}qW3&hVnYgtGxCDI1@n}w*(JvuY`zZeP1M0v_w5R(#aTmv8;lQ!7#2gW>g&I7jIvSW1f%$uvYnE+eM z(PCLHat1r?d4qn@UcGHsN0Q6c8T}7SjMs$8RVq;sAtD~n0NeR8TP#WdfS~at_0HDk z42Q7=-EhN)(8`_s>^fPc^~t513_c)VD&)p+GmuOD6TxyzQOwtxr|Wj>`j~N!^ll1F zi$au|46Qnx{EP4~0W=wzD*glI{WJp!ExyKDW zakX@4o98G_tzVE}l2SmRx#kB;Ov6^1J2Wda6Iwedf-CT;HHbb%T#7ZUueW!$$<^Lq zno2=oI9D#Ep}xKsfB$H;-47$y$Hxad56YE`#z)s%2>p!Fp7@!NkPu$Ix4TP>k6&fj z{d}D$63~sb#z;H7rBh{6ZoMq{_nMXe$P`s$z08PjyVeneG`ey6OBFzwYHj`B!EDAu zJ&#!<8;*}4$m4WEcy#h*;Hm<7gtmsk_s892B455hfeE(PU+Zg>vcxNlL`8k``H8HP z^jQpM0gImN(|$NfW-iqZ?p(`P=c_)s$+YG=<-&as#K;d^xA}g^v>_egExro#Wt_^_ z)>Tj9X*?}LCk8+nhtLm`W2kRQ3mQr8ezJhId12inF7ANXV~Mo5u-%hzvR{XXyHLXG zLqJGT)wIVQn(xh1h%dLU5rx&vwY5 z42Ao#EF_K>X4@IBlc6d>JJ-OqF5l++^VlUEpl5EW4;=Z4)@`*`eZY@AQ1DD+bA zp6!gEG%~TXJFNck+ZsxZAxaAMH8Mc@SOz?H5|pjS8yX&~_C(sfxU1%LalNsoCHqXlG4)A315j(*IPmlW?Mn619@*r zNg*RreO+^pr>zPx^-y>V@Hjo{&okxgYRK-4G#3-Y2ysFV59WGJr}qoojh=EesX+8X zF5g5KBbF5wtu+rOfmc2lT=C*TBq<`&_|KdA>+J-$`gp1}UJ*$G-!8&*hgIz1Uq}xd zi!Ch}0lai2X);2tnopHydyfl7{hzgu2oz?Ts-EN+5!pNu*rHgHG2^JJua%Ty~-yG-UU`ysHRQfTx=8|R(*h?({3p++@&iS>N@=R$*xKC3g zMqwH;vpB!lSp0&7&XxZ9F_Z6;XHj)Pd#8YK9MtPGygiK+GuN0^p#c!b3`P2SojO^o z_KyEjzKX;pp{J>3@!>Z0DUH&gk$8(yZ}h`WfV9EmX4TyhWC;LeJNW(GWhkF z^%X#vBHRI~1{Fl_-n~O!#(!S~us0`MYfDQ6yNVw{3D(DtD8wZq-Z|Qo8BIS~$2o^O zs;@>D>(usjyB{`dbSWTjafMYoB2|p`tCqshxyqg9chUl)$(;5Pr`L7l@{_z-d1RQ(&%e! z;pVw!+S@<-J%#kk4#M}f+)bAOw9{^A=BJokCEHJwcgiLF0^EgrPW8WavwdD-#pOxg z8Of0TF%f2i@utd64%DtOU7r6*EjXicjJluFr* z!y@K+rYb6Go^OvSKzb$Rx#@y|>aKA~L;S9vRlm(!c@=kI+u|O*2-=I!L33c%nUS%i zvWhf#Oo@p3X<8Vn+eFe3C@q~lW!+QITYC1~{N2jI7<1JA+D_Y-tFU5c+77P7TmC@nj2Gpo*8COw|t9ja!|h_U)| z;#{lTp#_e^o@|uy2`oNXS;;y~l}6vd%yoH9RqTZd(&4*dvkegCh&*m>Y;-pulrxYF zF?vhh{@x(5v$rBDGAbnGj;%nsz=+x5d{<3KNN7FW$YL33dVSDwF%dlmIoWf5P z?DCN2k|Xy{d}n9p{$}~!?Gm<_uardwx*ffe@o9uL$ix@r0-PUw*Zgv$F4d29EL&Y$ zj) z5NYhr$0d0|H#d@X)5`W-rb`$^{SyFv?-1$lh{OeW%8om$mrH1gm+BiPV}krgB+ME@ zzlj(=p1=CEuTMw6;t#9PU=E0-PHvi=ndMp7h}VE}x;-y(wAg1Pn57+tp0e%-1ON#5* zDWBeUf<943;M&@nj@d>|TZQ1=evICZz*OiOIskBOlirZYi+&9jZRqX@fzq39%2Lc znW_SEUAWj|Tc3jRYM)UEELhrzU&nz7enrA&AzNMuE1I3LTSz^DP>yf zk>z|%v%^lbEgnJtmARQP8f%9J3E$=SZKZXb$u zFArZb%w6fuw$g&iTlmzzs?=s9;$3yDjh+|$;qSZp#H3-bmXa22&#*plN9u^G^CkqC zqbJx1${U)~Pf7cg;>c0DE!-M>88vqbADV$7Q27_c77CM;*8DQ}GUJK-(O?HQD5w&xyQnRl5e8i?MDVf*uo0|NWq=Pe@4RL$&^fmpm!C)@-s6d2_Mi1M2NIkiu6{Q3-iY+wys6 zXy^pOOiSB$rF^Hp+IZL!6J~2~AG119qSqKI0*A^Y&+dG=VfUD;R$MQWdJ+p!doCU; z8=FS4HqgTg2%JQ=ySln6^u&62o(qe;*@8q*d!JNL4-4xv!mqAk8C};;)p`N&0AJr8 zNb4UrXij|7ix>?9KY?r`cWCF=xYE0-xwkqoi{L4;@I%<%yu}rhOe3IJZCQhXy&Jcb zRYfYCvwI*JRC}Nj=K0>BL{R9>4~M`u@dphkpf6`pIdDw2+evN z7eb?8TIZ7i}yIa?FZ6wTz1jTwKl45(=r8m_n zx89es64?oy^pKx4)@^%uQ6Q}WDr+2F6B#X1z}|cTAt5q~ijiurr}q6XB=pr6R=z&3 z&q8WJ9~Ba-SU+Cx2BtoGnj?d(QU5Nr_H=)tUIkX-d2`{X!OI#My{aKAE4%u(k(AdV z63ZFsvKNY)Gs;{W_Z2-Go^GfE3yH#yhxafUGVWnvDoJ@$FjoH0zj` zHg~7)53hF0Ilc*PPUCO0=XLe<-KqSPv=$wKFHcAY7n}z{_cwr~@KW9Sinz9E0EmHJ z;_2ys@Ez;>TZdJhESJ8X(AD z{teoj6Z3eK6;UeRAjxTxVU)8YX(L@OVhVb zOrkG5e0=?{wfV!MjSwefi3aWd2*l;GU8^5eTU%QOvh&^Do$=5wt=5*?&qy`~iM(%5Gp-PE~J%TNql(pppWH#Bmbu(fWgK&DFXl zRi}9!2MK0s&D&R-b^LM=cQ=O=n-`2V3>L+-+-&kr(wI3 z5F8Rte$VSF7T<~q(aoIRYx0jDp-c+zT%l2%mUBH>F*iuv()j)qd|zd~tbZrHRL4wCF2WeA5y<65K*>~40{qU+JBb%Z z#WeuA>y5S6okT^lL9PtCk&^_6h**tp20wmoO{3D2q!ea*dP_~Sj$c~bt5L%`f?ffO z7@FF2zkmNeOJS|3u0Dk{*}HFg4nGq~Vu-!vwh{Is>|e&NYiQ^Lp!FQ05s7tncCtkA zAZO>DvNn*!mU-Pfv6BZrJ9Y2hd+*BO06zh`DpArSc@sXmz2xR*k(`?76ex6oc>0?f zZuY+FM>;+Q&A4B$`=CY2x#GJ!?h>=}YMzpxyzxSp{rmcRdwE<>vL6=j&8Lw2BmZe~ zk-4^Icub7FSQm^)d>05^;1xhEd?#68S?o~nIyn8EG&V+ z-x${4(3#V@=FqplNoC|>;?jw6q#GKsp6f723lF@yO zFzKAa@SBb}O6O1aX;=%NPRu2|;Lje0=8EVQ{tfLU#?Klz-5|4-p}DypvWSn3ivybX zKT*hksa&e7UVwki-WP$0qf0=u!gIYV)oY}D=(@oFGAGEK}s-ZmZiyIh#Jm!kH9g133M;?<0zVNr4*n4jhRoHo|+HAoV z606;S!Mu<}z$28&0whw}+uNJK#P)*_*;0}G#k2H2TD!m89-wQtsE9sQe(T^MH6_K| z(lXWfJ<4x9Xl-z6YAO@WIww9Kwiwido4-YqY zzAEh_l=$>XGvp4&M@6+YDgA9O`raF}5ZRpIRoq{W2Sr#aI|ca8O@EjkJW;X|8(>H-J}H%NB*X?w{WlV zoAwUN)gGbwR`AVGYeMt@fsL(gc}>j_l6i*9>+7f0IcjQY^*$M#n&RZ*;$mPZR-V_! zTH*XK4kjN(j}R@r+rF-7!>T2nU|Oj88kj%&MR4}3NmB+wpI~`tZ;;5^fh${yIE?c6 z?70Z>E-JO(I~#+NA#V|Q-(!D z%dy>Elk2NXN=jI#>X9_BI;6RJj9Q9QlT(LN|J~2fN!n@#kqd9)^vRLUCA47@|CTZ7 zrt-XRB+rLTYmtNfiwk&;G2$FH*1@>LVw22bb#Jl}5y6SSHqpn*sZ;S)Ezv%3=JWv1 zR=P4Hs3l-xOzp3xST&%N?aWiB=hQV{q1QxrSMNoDW#}~^lU?l63u{Os90>z%teUVV zV_JQAkG9~^oQ5&BrGz{}*OIf1@E1u0VrnKb#?`9 z_{3oQKWTSsgR7PZ-Owb3xttkNKr^r4M=2LaA~pk;!$^Nv7Y7pmpC=3@K`bhXl-F1f zfhVXsG|vmRV}sdc&UT1bGMGST5TOOCGb`#QpR|LInVhdJ?UC4EO?!OrG+9v)Y@MoK z+;fc5^!H1xnco!9F!E9Bj$&;Zx4r3Ij(z)24G}FfWTYn%ZZf{PlitFFT9ZePa!YTn zB}dLh_{vt-xdb(>`7vOT5<7jEA20~ zxztq||GqT5S=sBTgcrhP-z7-r^bHK)*Py-lQSmeCt$T9|5u_&e++{_DD{btSKG@{##kstTALhs&~Djyu5y6GmKT@on(&HOUd_s zd`fHAFWZW>eFbZ7WAEY`40=YQGy0Co^+Pft2TE(`nx!cF9@{=l}5Q4Yq027F#zYkvCR6j+kL~)(Ie$3nYHNVdtY5Ik_c4mL|x; zQ8Q`p2^3!KnfuQ~r?bAP2;lqT=4KJ4Ve8&yJ$=uAz4Su`m4p-j&uqQa@k#N|MVzeJ zvG5YE@Xh%mfONI@RZPOzQXH!?=`Amn}=3+gWkw zWP@ae#&7H|_m$z$!ICzAT6r~F1T3V_+GzaF=L)Qc0e}bj zp%mi&z_%hP-`V-n)6vm?NbsSBnf}CV+u|)-Lh5(B@S_*TeB+Mwvq@SRTt;_4F|N}T zO@s2}DbmK+_h0NA=o;iPCVt~@&2~Q44t}2BPbRy?LGuap#`gEfwvR|@f+GfuSC?Mr zJw25`{feMlv0x%sV+2^+BwB$YOG$w>`^QnWWm3Fi|NN}T}3h1#Vh@gV?w!E8tE7@XnwKHwj zlyTR_R71;)8slP}?sWz}j<97;oj(2W;}5%I%=1!x(8gGqnvN%2gV-%3xZECtrM6I` z%nO=I>Ezn2FAzX4SKH|IWIiTzRk=N~_T00psR0|KuB8QM>2xf{Eqq zw4;gdgP=V*V$Q~Mw>&SS{Bevb7Z>H^xCK2o*Ya=iKDypC*2tSG^7bMep<?Q>JuZhuzpvd9>NIlu{f~<$^@jw2s`!v1Rm9F5k_xl%x+*!<%(YX@*1X?!JS<#|v(c-AN@8oD(Md5(ID~(U*5E z=ofm$HS_|_ygAfh3aJ}u$#%m55*z-6pdG~6v_B*}0H=|y#I5dTNN8G*2{3r36hlo) zj_RP7ZRr>_c$u}iPbZnj4zfUX6mN$(Wo zcAis;I3uyT-OLaIlbrTzt*@o7jRuBE0CA)Mpo@q)sW1E+Vmg0DKhmh_YUkQG9MgH! zLuZYF!5l}VNvyrPyR*g#+4X#{3w}$yjl%*dya>N^B*|9*^EQ$t(8zUw$t>@}Y?a$K${2oPJl4pryL=m*E&IkHTtB|iN zBZON*w5In8CeO;12`0Msj5A6_ID>zvwp*IFk?7Hw#{!6zH5v4by%_H}s%qu|Ls_I~UPzuJx92 z%%l?>;iZOa9X{7!T*s-)ffG#Z7ocD>wHafnz@gJE=htyN`LP~Die-{k1l8nLJ)V~Y zubCMq@-(l%St8y7Svl z=xTsG?8_J3%dLnZHa+JZZz-7Nai@G%!_#WqS>js0EX{G*111iUO&u#iuWz7&%T?)e zp6n-Pg~0ZC`d8HbX-VWa1Am{wiZP2q$0i*em^ia+4SAnY*UK6M1o??=YrTG_27RGW zEdg@9rx52XSUYcoX>whk&0QDxbpfw;MVXSSPQWdQ?X-!tqN z#-MQCQnGZebeY3yu`y}{z3R8csr+Qu<>rk&Tp%WR2+e0NROHkt)*^eL*q0fq-lebj zUGyNZA3xm7QUn`5eaqDC8))XRE|9(Ji$3{k?g|Is)e6W`$V>C6hgh#i^6GHufhqMX zK{rv%%kPtJ_JoTy@`K!)$62*w+{vldG%9c4SYab;GFnz-P=aX8j!?#PQCrw;^ZKL>p_seEkoWZ z*I8D%*nQCetg9TvT_IAYta#JR3V+Q({J=A0hGy}sgMM|~mcinxZrwjvI&m3>~h%S{psQm;uiQQt{J%M6j@!`I;AlG8d%a>VPjohh{yH2 z>E#d9Gl_=9EkDpFS-JmW0cpt_r!{xxYYWNRMy@%y4L#lcx6Rz8bn7bW7hb`%w{HoG zi^BmSb2K;W%3^8+a1YBS&4P=ErFnP0Kssy6%%m+1@?UQRJ;l#-*br2&h{APFIT;v} zgD~#|TDm`GIa}wadysLcn;QXy`mW;6Jf!)pWOVu zx*N-}me}0q_1w1D2_l>3kDnkL_s-y$e&oBF>fLas7PYv`U4j(a(v< z^t(t@$?B@cOtyx}@>FjxespYqybiQWmDC~?^n-i!Cj2l&gL|)_!TTS&|O^AO35#0Sv z{}3iZ;y8`c4T-zZO-c|hkp9}k zEEv$Lq&M$=0MLQ{X@aURD@RL=tz%4-#lk29#Iu8*`OEN(-VBVUP{kuS{HF5&yG`N&rZSiv@Rn8GU zzEJm#1~6_;ZL#d4L{7rb$I?51$hbX42X(zzZl-PXQRG$_t>fO%0^E-I}GUqk>J1t`V(@J8aL9 zCO}nqF3u=n4z^Z99sxP~k}7Wu1jZ#KP+57N2t!v@oA{9!fxEoi4L`x=x^}-l#hPA7 zNj}N8N~8XPahH&Z2N9suM$S;^a4zf}2}j%2Zl4;Hr*#sY?aey-7ZW_Z(yIJD3j3cQ z)hJ-gZc2B}eD4xYG`}`~gUDKLOajxF+cuVs1EsMncHIp?zFr!i-wde>@aF@21;+-lpc!h8O9^l+ud64cS-`2=1NQ8b&iT-*~$hN9t_!~m;I*@Y?6)mp zCBN1OXGVDPmjLPf*&Va(d{noZTZTHMy1YCpVr?o9-qGi0}hXWP{_o+HF0QeR}ON8G$&)p9Pf@zI^LgzQLIx}&$Wbx?H}NnA57-n z^(6~op2~JB)`duq&mxe0v*?T&O9UVQ$zVF}P^}Dpb#;qY^?>o%XQZh?1yGjtx{UY#3 zn|qeMIzoqhUGPx(P*qIur5I0vr7S~9%im@Gb_7}8uGU~@Lv7>q6QiP5S5|D?pD5SV z)DRKHZMzsSc+ZE0#V9}?wUZ6ZRefHj=^cpYL_ouEN#@Y$c+%yLy#QY%^8ivb#L;lW#-uTf#)FTS-_oe=D?0rQ_qC_OcNTRkQE7D+oR@4ij2s`F6!RVBre;x*^k zR#1|{A8AOq_(wz)gN0a5_Ls>^SiBwm&_D`Md*k`MsE_nGc;WTI-W)zp{U*_{Ke$CF zmVQz5m2=SqDP|a$HN#06K~igckZbs7lA(3Bcz0$y0U|~Lo&N5!yQ}>T_@|A#b`y~t zj5>cuKyY^zZ~IINgD?y%18>_O?W&bLHiOGNxF-0H@6Ar5Ks6!rG7ix1uDUr1=IqG} z3%aMRT&=)2h8(Wk1k1iiuOkd!_9Ux0+2w2d&#>}& z%6W`={ISIui8(h(H>XGIIbc=I!E-flKd<9;GueoOL0X0NtakmMk^QK3J}lewnLgJ> zQ9pe608G%1xy2KVQOc<=*v{OkemBegW{bDSa@ub2ye97En8Ws&U-yej1348DoQhcq zymN9a$RX2%f2Y_Ne3&bH>yT4ioLa|Vq}LyKb%APcK0Y?4tE-D-Opv)SM!ELpgH*^2 z$Q)%PS8ItRBHi5FHfqi{8NOFqlv4oMn}LFt0CHMb>et?yGJexRLodghU6UW?4no0!@|Pq z9DCkM`qi&9GZ{Q9e>Fo$z^S|NJ5I6(yhZeL5C}7_NJY% zb{Br2A9L;N(%6H{{iqZA-g4I!tXVnFL3Gp`z$7>^(%$I;KC6-`cUNb;GU^g<@1T^s z2C-X7cBlKp={QUi!mX~Z!jMeHklR@HeI7V;$3(YODpc2YLKQLIEf@{(p*&DMBa;#I z$_xGL1|xUA&`KSb=29XT$I-SIl;L!|cRi%LwI3i+%&IhDezZVg_-Aoa>~+1%69*TV zPdduwXYz!4&{;2ES0tp2Ga~(w%J+GS%Orf@jK}IN*B3p^X}5sq7mO`F>SV>Y*Q5Ts z9|_p8t;U#D9$l4)2YSm~XRArk&k7Fg_WlmRqYqJ8ZpZ zt4&cL;#6u3zy(i8ymc|3yG~S5Y8~)sv9>vtANC-z)SYGCpJjxnY~?8>cMP(1H*?wp zWrOkyCLP%6 zc|r8K-o2LKn_>B&1c-`jxlC~0{qidE52MQ3J7eQ^g=&o%$f2?IQrVqA3~BYhwSRrd z1qo1HD?me2AjgXdQ_x*ViDi5PO(P?v=#*#Lu8OTXQ(dL}d@>4|ryGCG2taJr2;7k4 z%2P*kBrSPpwLK5>T8Lhj>d!&agT_m#r|VIc5FyV2nSZ0F^J-E?N=uEu0oS?W(o}GF z%$7VRLFd_0Sk9JGcuBCX#=ROt*?b^Drvc(@(lHSh!L9bv)&G`51=8l-52KiuCzu_` zF}OS)Pb2s@VE)s1jBKZD{4-6=UAB_+l$hxiLY)l-GA|lai6Dkwi|$)O=96v1-1+j^ zc}aD}i=)un?4JI^ou5Gc#ikQFd3h(-+$`It`KZfIn3j_toA%bS#9kG|I>_}7Hg}=L zCQj<^|KhArn9p5I^D^D+Gg*(yC-KMm8{3b9QpVpu@S)mR%HbQjwdX+A^S8I_FbR1I z{gPWVmDl zE;2ZtMoK0Chp5)2yI_yQe2lTP1mrAPTI+;aZhX6)G>>H_EfcC;aVvZ*m95@-)PcUY zj>#EM@w&AHDew1Gk!DEzk)1ts|7f9kw5@zX>w^Pl{u%}R|Z?nAmcvQP*A4~?Jw4PZhV zlU;uLy#HbW)Q|tKy8M4>MoTEk!^_LlST{FYfK2;%qG-UI%E+e^lNO(h%y<7g@!zS% zct2tr-Sx~#;*hENChV@Ru8;GX>FDT)zVS9rIN3PJO0|6Y*OYXi;pYZYAmT9;s5Hon zDgJh#M?qeFiX*OXYOuNFZzB}vGE6jOdHHG!)oXTX6fR0Sx_))w-pUH~e?C|E1pa znmSqpdM>*z4*FIlzZrU_eivSmm>CV^snW*DX4zr?Q&M49`nQq-M~bx)0WC=`yy|)V z!<4GDd|>fQ(!e<+&q5g^wWw|MEJqze$NH$A-qhHbO2&+xxZ$4__pZ?5jx?J$T>+EC zTWQ1PR`I4AcUyQofr^q6@L#gKQ{Swu0S?~!^+xY0xE`WR{>~AFxfIVC9-Y3=(n9f)i(|Lx_4<<>fG$b3q@)&Rwg`#abq%nCgUaoEPt3R7Uz6Sm4% zE~u!i{3mj6ns`{RGHpjs|KMWNwGQ04JNKCqkbE9~?+ydp%a=|Ps>QY3_m?@X8lZ}U z8muj%*N5@BI3QDc&l`22K*bP^&}JK}>7kP~8fgNLJQo zkn03qUZl`Wg02&WYcEu>GI%a8r%Rh!{ur4#+=0*S@3qM8cM5}=1Ev8i8+RD};?8G| zoL214XLSVsi@o=bYHI7(g;BQ^0TmGe0hNw4=^Ye7>Ag3R-g_s(28akKy(7}QbV7h2 zz4uN?q<2CO5R!Zg*n6M%{l*#hj$6(-Ks8+kDbhSBou1 ztW6RbZH;G_$t-Hu_-=L;yetr+nOkGtZqi!Pd8ty z181o3KH*S!v7h5%_P5y>Gmq|MsCM#jwFLKk+1$&%DywDoH%f_n~%H57$wo?s_*@ZE94o1L}d8q(hhL zdXnMK{)xok!fxvn{*&adMr}L_pZp)kg82mt3=d6FGM`85f(W0Qzd9tx3=vJ|UQy1K z@L3%aZ?hDm9xcKz)8g&dtlt2zbu)VZkr4sN!;-)Eb4>V8LVqDnQso6TtZB{ z?IXfI9Ea#@bObkiu1@d~!go~Ii9qEXPq7W4q4GcN+>%zu5vo4 zdP5+M3QW0rMV{BwQiVHvl{`1{j(X00XLrt#)WMbgdZc0@c&6B%bDneg&8bgqcRnvEF??LOW5CYuNanRvOP~w5DY{HJFP`ewD*>7=mDYl6O{4z zSLs2xmB2~Yj|HYrQy)15#`eBL=LvJ&FJ4RzYy0GzIMydv)3d(#=gR()AZ}KSvrSGg zoEBp+kf4R&`l6;;(3Lda{wwcqzYK#;U~m3*=W?I_f&0h4wK^tn73jyUmF+c)9(L6RNK4lgeo%QYD#hglrP~L~ zr`2J4Jc_3l-YFzgs7OzXC7-I{@sHti(hM=m-JK)$=Ch5L{%5Y64250R1Ej zwM=bYs{38@fO>|d3l}ekN8K)%zGfhE+QhFusYg+I0f%zphaKUFG;$0M+o`*;M{Hbx4K(!;aD$!)6 zJ!ZYEkFw3X!fIf4wj~58lek^&4(R0MYDD)w2}M;aWs}DIUSCg&iD5kZ*M^Nuy^Fb{ zh5F(5pT0rVdIJ(L*~+O1Kn%k#{_9t#O}LN=i+w8y>oGOYC(@T-Fy%TsX4=$}VBhvf zUgPAXUZGZEl0e?$sZPnx$CuE5#=X_-nq6P(0JRaIJb;OtyVQCpi+gw+5G~Ql2La_P z=#43+yqu=y;?x7#!_6ZW{qET7_j#Sa&jq6Efl}Rxx*pBuYZ~891Bo_Ys05-5K37GnXDzy@*Rr z%c+bF&;H3HDh4s1kbTF>BjFxpjB(1YBeawb!O(zy7;YiR1}35>&qD=C_?=(x=HBv) zFL@|WB>x_Ad289>J~L+l!W`vnnb2~0KW*<|S#DvF7k`jJ+mGI_=065&mbLQsaSARs zIWmzr-OV8=3sw`O4exq7u<|^ER@m);!_jyslhwsk{H(v7+G@xyvh-bG2lciT$cB!C z2J03Y7NtkWuiQ)&Y+sshtg>{}KRozaOl)TM;LxW1YgH^+0kTx&7yox#DpQ7MDc61J zsxKG>L_NKvF}Warq|D7{!I;e-K`4x;LslHyBwq}eJRgMBls?oMF5m~hv+Ut znpP&83Vxo#!Nl_p*Ae`hr(KKySo)fWrOnObE}B5WwDf}8p`)aIc0+dTvKaO9d`9Z4 z&T2*`9=@T0if_ugODoNP-Y+Qyo>8cfP5 zyk{~W+>3ESu2~I*4L~hZDY^^DdQOD|?#%*N&8Mt295j;Aw>j z0fEk~f2Zo<-?x$HUHqSHnEyu~8n6T8KDmH6ldUaK3~{7WEh-?Oojug?60l~#{@VHN zSie(VVu?5K^6}|pI#u|H4`?Xt_-`6^`G;hbl*dRsuIkx`E99I;QJi9;qPypb%tTC8{ zQ1oQ0X*TBRauWkBF>6UdPjxYjW=sa&3%E@42Fx4SOXw>m~c`DejGe4~6T!;we^RbIF0c3K3V!@#y9#+=8^)~gN>13 z&c_qm?@UNjH<$IRU*O93DS2#V94DQd|Vx@Mh1CG zd)B?xrn&`8A<@y%UsC1R;DE#x2t6&}0I!~`uq9{L4Q_bsq5nKf`78LfQR2hYZqlx* zF`jpKGA3c#*;mR`s=>M!FF75U>LuJd1G>qS6f(W&%XFotmZ{F5O+>(iBs!X1$+sN4RdnoC)g|58| zYw6E#uUebeMS?}v3O#57sLY!k4DRdy==x?!SeHa>KIh>g94-rpg`_}9Ab?(VKwPS4e0>xoix>Dbd!@KnE@;E<4zZSzwFzsN|S zeokxVbHJDHfkcF;_?h^~EyDso(8C!B$IK-gOh#*WWUy)?dE1^FywdI%ap+2ONdh9? z;;oeHV_+|R5WLV=Ta~4=kZ$VN*o5(*;4PGDcxl?FX;aGL3gsx#f2r@xz=sbo{WY(< zvy^E_8=6f0#D@OQ7y^A&g!c12bj6;MM0v^ILBs)v@7h=~3J%!?lvLGyH1!hdIYWz! zi`gEA1j|j-2|VLnrI?TViTRFOXuWj3=-Et4E>q=-FBe2)(!yvGDsYgrmTq`4NkRLy z?HcpSRs4}B#n`VQ)vOmiP&X@%F+rbWK}JV?9>hCOBKE5!wwrmJ1GTC` z8+q+T0pnDu>$iDzVA^eOQ%v=IC}JIgWtqn#(=EM%oRn45futag!o%~Q;uJF*95dES z3<5cyZ(L1p#4N5&`2`SWWHsgm>s zQQF9-fpr%28^ZDQ2K(|nbTKlAVP3=4*-y5q7VU2@z34Hy8NI%)0GH<6?O*i{>YU^k z5lIxvID0KgiZc%W0-!x9frh)X>y?j}ns))RjbBPM=2}CiDs0g}dW4#)a>Sh+0WvVN zN@QHCyHAy1q7tCQYBE7VL5%~jw{UWi+T7Ew#ea^PV%8rqbo^Y|?UNuOBHEZJ%f6X5 zFCTB!ZZNk#Q3ho3fUzpB-W3$Th2TDFsQjogw%Bx~%1sR2#qN6NyvVMwpnz3J91zNN zcziwxon`<|B@G~@K$f=VRs+;WPIs7A(|B!0-nOqz+Dk_k(PPQB5du@(Errcz;X&oP zPN!fqCm`#ob64#Aoq>eN^kKkMphEg7(0u>j(nkuxP9W&er;exlGs3E3AS9V+vJa+O zItQW%|7{22?@fjOEjtANMR!vX2R0a@<;;Po*3^`amN0f{adFj6_$X(@W_M@jJc591m!o zgAh?Ydd#m{?*Xc;_o}5&=L>x(D?MS?-)Kw}VFnP80Fnp`K&kr;0Qy;9|Exr=`OJkB zlJ@7yQ%;kVTfA%9`sMnK8+x@~B`Qh{uBm>Q4%Z78qv?^H`uh5+3@%Nq^txxi|J|F` z{6MXhHxEd-fJ9u(dG=#Y68~I1_-Q)}Uiu{Pkt635#}MLlaqIY#*1ca%ARFLRoOYbvJ8x!$DZ^X~s(q{N*oH(9Y>Hv!Dz?l327g&o44 zA9Cp4;V>=8NRhv2dNVnXk|<7e5r(VSpGtb=zOP`U6aQ%~%#>d>Z)wh1$ilG3&ttiN zB4~4;Y@kgsDsNb0`MigeOlMNqfSksAE>r+0!I6GONp|-x{ZI}dh8UdZdc(&#_}AFn zN}AvaDE^*$8?mVUXqP*y<}>jC2R z@URPB5E6>FsM>Zek6t6Ry~PZ~Su4+H&BHMtl1Tt8LeD3gc2cL~uU3njDc^7WR9t_& zdaC2PH>B)nmyu$v|K^T?J~7QGT=Uw?zAfUJl&l*QlfrTVoTjFcu9N{(mCp9N27Ls9 z>(O+lY4A_zT%w1K9toA0k4}!o6r=j?%ayjZ1p{UL`81cyi-E3pD4h1s4<9obw$Lr= z?d4O0Z)^~ZRRf6Q%=3v+5gBE%#Z0 zoHyDi+S_WsR?6HyF*&*C!5Jx);QLLj*6ZykW-%szDjJF7(17@h<}OZ7IqTRRH{oeu zd43E4va1v!SDCZsyqyYJ2wERe2%Y>W@p~nQc45rKfE+)3SeK!?(AC+Qr(w*btMj*6 z)&MH8ZD`O9#FuD@9;DH}i*aUI94oe^KDRV%vCQ}Wt>Qm$6?QM+N$28*_8Jzdw-NfWJ3uFauKI?VZw}@E7L;6_r~P zxZYv+rs3@cq(8pSZwFATk(QAGoW(o+pyXu3ft4SuuN)o=n61;C{R6X<`~KHeIHb() z1W=0+up9rbmMiSON}o3bSTsMuoQqv5Pr0>APepAW3luz$IM1~=AB+r20jXjdAeaF> z1_THR?(FPgu)MUG? z6~~6|KFp9@jvUB9!;&LY;So$N<+1@kaqPi zETHvWap3)LGAvDg)G86{ux-h>LL>@08SYiOs#xfNF-63YNMZ8aVn#|yE>z}^Cd z??gM31dwjabJpzv0eN<2W-!&leQ}$e?^o-R%+#XGZ6|!os?1koCw)TWD>BUOAzbI# zAY1;)-zGjc@nwApm-_cV{sJx61{cbtf_lZ>iiZke=dtXzGP?La-N9zURt<%{ad6Il6kkAQI7l$r!Sm# z-H%xh4xpOY{#Lm^$M5yTBVRPa*HJ}9<;9B^GZ6sb9uydOuRbbY571lsX(Hu(sj`{A zy7GZ&0VMxcSjr4_qZqpZ&2!qZh5vq*5FwV4;x!w5s-Xoy{sHo{v$LNUDNix0%U4Kf zP5{x%PkEhRbSP&;W(T(MW2*JlcVH%nd(OQP3y*F*XAlt~^&IFDE%!mBl~%F~E0LZJ z!FS##{{H?Z*Ma!_zKF*<06Ps14RtXuuC7i3%IW~3Pc7=D^4-GqziH1E0adGy48wVv zYryFcfSWKUAF|>tDJcOooT39R+wjM8OaOZby1JS_{eT^m%R|f^6B9!0?B`TGz#p^( zt|xQdst#>`%R)y7?5Y&OPebcsxZpVnOH${A!?zmS9w!^9$Qs=VoLT`$f{WiP>g?$m zS#jG@%^%@*JeO<9c?fLFg#X_fRG+Vz@Bb&~1^;@5e`1^c7iDMv8ijw2!haq~@qcxa z-!nZ!5ljswHL~FsPvdtW|94t@c*@y)dl}G?${n$Zm${yKx2M*OF>}BQ2fbrIGYinhRY&fPOAA^cXf97EIhRhsYG91{c+Hz)1`QLByU*>@Vr z0O}10jO9EvGc`LUYvfFvy@ZOpO+}5&Wy#juf`9l>y480zn6lL?QR4ItT)5mTK!p+6 z$T%E;F9_aJhWY0KpQ}jo9Ta)M9Y{_;fqn(r@%{7iDT5_1@Qli#`QqWfT>#(y110sp z-+v8?b4GpHuBn!wlTBAhSJOa;boNM!a3%imTaG^txAA6>1!#hU`YBQPV$L^FF|ocR zAv&Oyihw#4y@O=7QJ=>e{zpYcMRyvmE-Ee=)YFw64D^8kLu`Tmw!eRYVH~hwmPZe0 z5dwXl@R{Z`_^rO)h}(S;{w{q;U7a5%>WLmrb)S3y=~vot>Z^P9?w1LPC)Ey;sTDr` zbLEHb-07HXj03g3FA4Y}%F0}UGg*L}@SXK@#4*uHDriGKUZ%$fzmk5{b8%sjGIq^v zGAbnaCCR2Rn0~eXD$RlQFx$pXW?8oBiv^(J09QvvUH6o4A*qW8&1wRtVv!MwE7X5| zT`Fm?j*u>E1+W5lS}r}H$y3B}hiU{g2p5nm(9No6^QlEP7#0>jYWxWygQxuJ3E$cpuxz z#|F6I$%8eLT?9*#J@8QYAfQQ;0Ms!P5DYs-uRhvWX~Ur_>|mJiRBg6HmBjSicE*VW z+ktN|OLL>br47%%x?eWFZ9rEAe@+Vmq8~mA*VDtoB(E!?NMgf9s!nur3pwap3q#}h zK+o~>gqD-;)a$B9ZJ;ysY0o?3b$V%ZIC)?4z35nYVFGh#7aA=kV3`0bepo|EDSB;fUaRWYVYq*(`c&7FHm>fg%eI}7g9Aig zcPi%uV1mbd6L;PD{kbnV<=d~{042kQ<08wj!D+1v(7Qw#88t>kz9c?%D_9dUC;l#(>}F3 zYio`$1p$!vhfqrK2T}l&IOj+M_>!Rd0zm%}2cm90ePp+>2PH=4sSYsF^)H^yB8RB% z46{2KTKNFl-Q4pKZ8UU+PhTB$D8N%!<0OoV4E zQw(Uqoj1oXT_un_>j(JTaDVJ?2J64DfKVZi!{gKs&{2*ftJji8QPUXJu~cmM=MVr=S93^tK1prJ(5h!fxd`%uOamSRIX!>=aOuBl_h_QITQ0;PBB- zS167ytv0(J16{r9P&y%vh;ku@uTNbClo8&Yfm{NMgA!{$?gTg;5Z6nL7rUo=#$Mrl zFoK$U<=S$!d+Mo>sJ;2?s;X}7qmkJFkF>|D_!kw2FzJwyLg{%nRXVl=yyELpV|ZuF zEU{}FjUWMm&)I|#U=(wJ+d5LF`VblJ41FBE5Dvq2w_U2|Q#UBo3fZA&Pvv%ix=Q@hj$1l>w13vt2WCT~Oe0T0g7FF zLLPaCJDbfq?xwKDAv#}7DL6X3zTcv?7Y=@*Kagf`bjkAM*b{fyhP={b^h_nc}5m|dv3Xjv3sM9a;hJL2@szTS}^S_Jv7vpnG2j0)n7K-KIMMbGp zJa`s+^^8~V0yakKY<1|U%;xU#J~DutyZs*HC`e;mP4qPKM+aGj$~VlR6iIG}WQXaV zgtIw$?2bPzRyNEDXBDTqw+go2wKPRwkP?l>ZHcL&%tmSGuhKk$fj;z(#~Q}rR>Ppi zFP1ynkUu_t?on^^t6wQ-WpMgE9-K8fcjv)uY29~)RC41dB{dsSM~)tj^thv$x{_1} zrGslAIb{0@`z3uS6Ru)2wqk0RYLJ7?Ii;S++6J|ijkw7Iwa?@i_!@*i*1@LsRV9z4 z7AUHJ4%NSj@!I@I1D;J?Ky_X*b|InWKPqedl>JQSaN@`msf!DTgP@N{pLaRbZaGlj z(jK3@9SJ3quxqj=*&Tg>Q?3_rtzyc@*1-7I6yxuC!;UAIE?pYwm|E-o%P0X4`Z8kp#NUaS{jG-w^Ruc2ha~Lmiak|dg?VTV?Ff5{fU&GDFILg#3>m^ zkKQJAhSeVLos5?z-oA5R+v{@*s0s=g+AF5*0Q@MqKaN}UpIVSGq=KaZ>jPy0Y-dz7CWzF5Zx)`}X-kgdI!3g5rJzLysl>sJ?(ecXQMsl-hxF-{g( zN<)}w09ZdQ*hu_0cHuc2`WgmPsDf&^@cc!T-A6_r^KaZVM71AZ)znvF`hUu-%KO)$ zo98uM^kcUSQI*6rl&n8)J~0J@f1u2yOr(^Hnqde^G3BCQVj_GUG{p7$E!C{41ACX? zo??+2(6s6ion5Qr3=yxh1Wdq3f1qMIFc6c=Y47jZfO%s6!LShrmf$_iy=9`{-RqFL zHCuH&T}3(aMAQ|ulH1x}Dt0mvH9rbGh$d2o@2w7@R-PEcdgjJ@K$jX{`ht?~ zdBF7Oak$Y>ee;#K*OIAlJlxeMm4=hz6;18Yjdv@?eo&;f3R;)M^U7)Ak_rq+>#C=! zk9J0C_N8TPYuu9XdE!0CR`bpF(ZbRQ#7=Y+TVS{0(e9I?0Z?2xBShU6xZ zn>JN;sN567nh#Y_znfks;Z5P>9Ii1TMOW3Ri5M=X9*QjnzhT(pO_+aE5ucEl=S5+O z4tvf9!5xD=Cv@DQEKl3A?s5!SiuqWtIJeBwE7m++l^wL&fxeBm)i2HMAzblg+TR*Y zGB|SRpon^Zv+#PN&-&KzTH$rB3SY>RJ+I9%ggg8%UlO7<*FPxAA$~=?Hfg<7A%!PMn0kPZxwoZpLQ{^$+5yH z%xQQ4e{%EGo4#!+Pu?+l>GOJYCADq=-z=SKd}~O!wD-;Ix^KR3afO4hgB!z3qRzqc zqahbkyUbX*V6^eEsUh;iQD>q zk@@2Q>pp6Gsm9&kLaw=3!XK`s(}~xTF}_1nh&-8@v#bH)|ATzvF^=4Bv+-yL{r0<9 z0$#=L$>`YkQ1>-Np}tq-V_Q1#5tFXhM}<#^%M+W^SLzQICijs<`$aLWcx0AU=+;`j zS&`_*CF4YBbx>;m3&#|O=Drf{w4JpQr_pOi9)6H&M@h0N55wbTOUoBokdO8CP6O(T zdX_l`)S~@cPrVN2AgN?56k_xH#!8)xpuJEWTnj=0gk8LQrvXs)QyR1<3Jk*qGqQ8+ zEHS7fzkC{j`K%?Teqh`q^0l{!TU+$dHc=g7yEKeWzJ&kaP zsffGZCN}K&Mz73E5%O^MI}+Y2bZAE-)VTHhywdTV-~oKK&gK=He3@E`pUZXPX`nU) za?FV!k(YxESyO7CcxZu^Id%?sr>t-U7wkmMckW9sQW+nuH@{_wq_v-3s)z^e?I*(v zobN^3`$aGt4_X#0tN8EfA%89*#*kiI8+P+QI+Xo6%5s{WQ5e_c17Y`5Mpk)AGXKsRGmY;@udg~P1cUh!BEW;Qr=Hr>O55V7pyCCu_j9M!$ zh17hpchri4Z5)<=`jXOxliR-wMM-@KBHo-{xDA@45=s&A;^yn~BOq{ntN%sXy3KhxXdN z*&Mn_AX)L|%#FPpvX#&&Rn8Jos%Q7zr+)flbX`baO^(ck#9?aZq@FG~i}jTdW&Go) zrD@P`zf7=1PofLGwB_7rIXY76W$wLhy;x>~M_~nV;1_t;(Sr93J?}3?jDfeN#j!6f z-%cn4D}E&SG=cp7hoOw$JV}TxjH|p56*DqPtW07x{XIs`ELlec6GTW7PJ<||ycNk< zG5v%t^@$>e1VybP;=fS;C>&#=8{F=wHX1yn{e@~(+t$?f=;|=5;9E5!@|HZV@){srrWF>G zLL#!TOrlAT)u9@?Hf~t<%)X$RpOFGqcEe*U3 zQQd^~=p@54FmH|6%>y2+PCYqnV5O!;%WS&9IgpF-bQv&O5oYkwF=)z*P(L#`EX7HDY`5{bH$^u?35XA z5a!VPxb^sevR@CXt0iRqOLoRcXLwp%wbE6tdF9xi_JDtStC4j_K#0P(5}cm3BRgcV zUs2&lwUWx0m+UpXca)>4kD0m~pB^_3TiMA{9_m;=oa}kOKjfPGA#t2`nu=3^rk{CX z$@;j;qO5ZNtxi-5XhfD*%SG$l%ybCUd~AjI6J&?oc`AU0^^|tj$|`ZCHaG8+4y& z-96$3FsHj`RQtEwBQ~R+hfV0=BJ<)q*@hv7)`L0E?C}>kmAUI{B4*fosZvL67xkgw zjoYMCl*TFiFQl}UHpOypKPOG~$}|VX??ojOL@_5X=;QaoP_ypOm*+n}ssCXxS->{e zi{4a-mN205woTVY4M`{li~3c{mz}(^O!v%c*9%Wkpy@A2$Xd=vGcAK9+3PXDl zDoSodCX!$}?N%GbHkVA5KQpE=^tD|+np^Ho7aqcJf^Zyh5{lg4R`*qSZhH$>$72UZ zf`7TGxUhvp2}oNeCmLU(ZW1wr7KW8<`i6?HFMx&B^)7x{kI9jDNWFe03z|z=k4* z!Z=T3GGBXv=5w^Fx%K?iZ`N+rAmZf(5bqm=%~49|@{>-mWxmT`r`6lZ`Zq_E1O#%g z&#dWnn<~HwtgV6E_G2_=R?SlVW|sprG@Z(>0DGSh=!*XXG4F$v$~r&$y+kixsA2K% zMmgo~h`+Fa881zzpsRbiZ$lFu#j;C1J^AFP9tvvQ^8o!$W3+huAu_mwNY5k3GM#hy zQMJc*^h*z!DbELps@%j-lORQm_=Mg%cGs+g`*=8SKuXbIUtoT1l^)SYi8w7Yx1UZGVA%VG^?qhb>LaW@{&s5!u(*SIrZXE z$BFhW{+Rt{;#I}$z5H!^^Vf9Ak5^!W1T%2R7K zk|TBBh33VUGP?a;dD_D#$1^jeO#AhezKFUQcla6s&tEi;^5vdZ1E8jiicTnrpq(O2r>$ z2=xkyH&VYhj@qx$!u$2THR4n*6-ZhLMdD*%Gb8y*({0Kw+8u!fdQS5`-s<)zu5Hn( zbc_|f!nBEgBTlScK9)qGV*=$#t%x!cb6SxSc`-|4=c;w2d7xEecX6sw&rvN?;#NzSrRd1@+ z0SN!f)F^iSIyVzR!v>)A4txbjC9O`UIg;QfS+NA2FuHrRKrtqqA5hv)2>F{Vb+&5( z{P;iosFVMZn$6Sp07iKqzdr5nq_v+caN00z;z80H8dIpOUe7kcB zfD+td`PU`T4gczme@%vet%ZM?g8zq2L9YCb3`WI-yRS8{ivqCgKuDXk{5tkq#gZ#m)o_gyy*GL@`#Mn=<3xX|g=bkHS*500o zIxn1}K##E>I@$ZqDM{pQUnu9a*Iej@BH2hrMH@T2o4|(+OI^_tQZ}>6UE1BULbce5 zRQ_g!Ifu?se^2TIca#ZlcR>m@%YedW;&U@f=vJ50SdVtf9zQqJ63+MDOBiml=ptG zKY}^ebK;tLd5dR)SF}&TPK!Msb<>x}8_$zLVJ-2y=BJx;Twk7$WayF5*vG4Lv0Lg{ z3DPk3CWwoduS<9V%0Ut*qqdsavyi@{gKcR{8$!3#OXin-S$-$UK8N_xW{5;#2)3%s zJ|o$j!)ptX*yhPUR^R$jr#%#NTx_Tn#O_{ZxDT(0i;7c zR(myDAOWsWqW>$P!@K(wd_ zDoMmKA8XZ@%VqDOWn(3Qhv$jXB@)$Qsouk)>r|~g9dY*V1G@`c zlr=WQ0zrFlxDJhbbyj}g=aLQ<>+7^b3A+QrnS><(FHx-I*`f`MNKwUHhq^mLgYdC{c?&h0Hfv zl#X-hhq)W|IL^WKzW?rz%kktCXMFV3*5yCc#Da&%OH8lMtBP5xbPqOmgEtcMh*Q24 zvLz*QyLpa~iG-)f_NJ)wN(j{L&f`8$i&X&>4pw|@XFB_%xy2)CX~@d7Jq-p`8`vwtaQnrHlW#hPH;~N9zOso1#>d^TlX+U0^N3V| zeDSAljPWE!f`l43lGcR6uJKd>FfqZ~w9vtuxnYxv{ztL?kXc_^1MQ!`Q=A}R{7exo z>UazNNFh~3KmemwS&iFFD#lf=94B4Y7Wq~)|Ct%{3km0ktZ7S39_J%L=8F-qBggpD zknJ^d`ti-&HEx*4bUbpj&Dh8vlV`cf*{*PXru(XXJr?c@LHZm~8Dlrv;)kYK@h;P0 z7XU^z@ILdy_BK%L06gj|CNdRn1YP;2I7VbO zd2PwKwX=F*&e+(0E|g_vy`&RgQiS6)s#h6^yy{?t4}D)8N=U7U$IUt7mnFZaVjMAZ z8C%fFPD^}hQ85y;kDm+2o5^15JL|<~GAv)^IzW3&65hNq2gBH@tjTeV2n}c5_INn{ zc@fz_+!WN=wh#(y7DPk7;7ly zx^n$-^exH+qwqS>i5<7Yxne)Q(>Zq2JnG`m8rd)Nsq5+({@`5_ScK^6D%ckz zwwBs))RLm@ab^U>3svjPQE1pv3+Jjlsw1o~bR{^aw>SY)SWk4d;3k@6V4{J4smCY+ zQxUZ-##1`Gu~|*`K#*s%=9aiPa{d_S$oXb)KX&KA!Lh(72gvstO1W#J=zF+ z>3FodJ&As9g*mKQ7ukFj!tVVLZA%xLm;J*_d29@IwCxHF3o6gYwPD3S;P(CbK5j)z z=<5(Yi9S$0NV2fix;KuNk4j-!& zX=}G7Jh0*)4SmI{=MuYcD@iiXnKk zKlWIl>-kuRF>Wh3^vn#rGOpPM7(NC-2$V29K7Jo)@!P&wRZ!qr;ei1_7d!prKQLZ61;|V)&gvHW+ zUa+90wjmnJ81<}-YQw3cpHH`MxP*{TtLzV^WC1rKa3Z2Clw@To-usmZ97lMP9@ae7 z-B`+SnW7^NyWCqy=59@>buY!oT7UO_bO@s-gJp9}>e1GS*XxJ~b+t+_8Q^3@1Po+5 z(tbe>F*s5Ng>sh;wRu$e9k#|0I?ESCUPYxm1=}r)ySk0plDSip?#7C1@QMk2JZJ*= zkmW^BVxrhNdzFZj_uD2Lxpb8l-%mX^_crVc(`ubdsx+}#lPC{!o$J#(RFQQ!9!D&u zSvKVKlD-}ox_vVa~VY*91o)E%9(D|1a!wq?g`r)0ruk1uKuqg7SLRm&=} z1XAKFod(LAiXGsS2?_Y$5mVf2#L14ale%T*Qi^f8hfWD;Qjr8z1?Z7 z&GQ2DL=4Z5Zb-06kgb7_rUR_?^X?Ev?#O?RB*t~Mu8ul2C6h>KB&lnwdgja?trjV_ z!ZmirlXTNe6l7<{^T-8;%QF|)4O4i``nCmEV;4Y8i8@tSTU#0*tED@sMW(3QW9+oy zqr#tdE9>2PQkT$Nbe1P+JyZzmM8T&Jx=N_DP!AbZ6-_va`OI&lss~~fuVu7yhvYClvW-cEnmv)+>5q2gUH4TRBvP?RSms<~MdK57T(d-Y?qz+^ z8H+a!QU={Y9xvx^7DOQeN|X}T^7QArl&Qr>jp80W?<{YOnAN4o?J#XB2~4>XTvs_V zoUHJNnz#JeJ>~FNEy$yL!2Tr%J24DCDN#?Mq3`&R&&q0vUp}$IN_J*`cz(XbQvAV> zgM`ix4Co^8_EY4CS5w|A^7h+w{3_kXulP70N<$A3iDHQxKHviQ{)xP`9-~Bw0_t z^bGhqFeNLaglnEWyqT*z)K)nX&^@)7O$**=5fCoBSKLXMmcAAV9Jy}I$G1Vc)SL2P z)({ZPasqQ}$sv9u%}sSBa%kx8D7<@Z!@LOz(&&_SIM`C`ug{ z0P{Cj=fh~iXLdf5$;OcYZ%sbK+#$!}|D@rNu_=Ka}wB2!Nv6g2t zD&vRC@duJ(meuKr?eRFJEKc2hGgV(#`DMFn_L@-Q7=9Dc8?)!9{1H5{_ zQ#oV{QPG8VM5wukilxGzrXlgfvKcyXzzO*Z$BPLd5eu<8oK%%@`V zDjRP7v%5)p)RHNeQ1N?acKt9-xf_3eGNv(eYBGhI-*()YB4(=m`Lg;iL!NUvgCxq& zUDFKDa+&_Aiem$9e_SvrQ{a>6Dz}YNsAKq(II)f6%D}+U@St(k#gF)ta6BsV*%R@j zzO98YWQq+&&`;P~UK?DOS%Z_1(c`8ByxPgy;EyR^GP6pM3MO&$(G$D|Ce>?cM?(4d zw**QD>yK;0pBUS3*I|#e^GL8T?a`IYPW>9U!8CCZsMg5Q_CUla;ao0;b>83Lb`7SO0G&$iyGEIjjP^;u$^_T!OW(U9*2zrBvq zif+l2?ELNH3{UTOXh1a$L@yPiSU1f4g8U*}#ocLgFz9m@Q&2zHYboZ#Q0M3Da1xTj zp5v{N*n^M%2Y*V|9(%KTbZ~@)$kTo5zvO5}c z;M6qU@AFSCM`HRw&D=-Xi!_VfKxkttpQ@@dYf++lzOuU}Mfc?+7FfnlS2T$LubZ-d zbuCwM9_z>9-UN0kn?`u)!et>})?Nli!#0HXJ_ZVXu4ghaI1#^n$ixS6$B=X>xA+)= zJxwNK@=SzlO;nX!TUra-<~HsT54_fAyF@v3(%M?cR3@j@lNza|KEm(icINA_i{3;7 z%JA}lkmK+&%k;qX?0enqH{4~*S}Q*m=*~!5YOQu)?F-pK#M(I_mt`M``IVbX9YJPD zpv56$eoFWlj3X<{1O5Hkz=X>3+RQH3Q;b9Mg^a}4gqV25$lx8dEV7upeVwoVzt*lh zsHwG2d#|6DD~hopO^u3(bd@GVzzd=jDT35ct{_E1Z=r;sR{=o*fdEn?N)dt3gb*O0 zAl(pY0Rlh|H)-veqZ>_Oh04y)ft>S zDy&kb+85589Cm5jDJ?BQu_nBB z)q6oSK*yC>TsU*6by>VT#JeL`4i7ZUNi>2tDnbZRpC>iiCU;}4UsqJ_vmrf(IY^04o&m1FbXFNWCK zK6+%<^KANJz}I)J%K3&U3=ikFO0(JrmGbeKnoX*g#0H)GjU8-_j9A|=)sitf`)vM# z=!esKMhXrMwKrd^(o|BG<8_HL;KfBxXx<&4R};c_M)JLy9WR&^kAk}!N(f`AX>Q$gKK6H*0p>o7fd~tgW30x$KF9UY?5kD2g)J|itFB`idO)H)Fw`&Df1y;1aArvxw$#7D@-YUA5h3X2iiW(Rl<$XR zfc2rUso4yGW5g4atFmCTH%VuSiL2wb4~CP_Td`_gq(zb9N(X5)B1Sj19Wp%k(EDSL zjfP~XXITHF6&ih-PFjEZOSa4h)(lw)fUB$6tNoxR;quDuTbTF0l(Dd`s9GB|9P>CS zA>mN41*V(^!x%HD&#!x-P7SM87Do5>$A4!E6Ni6eVB}5Wv~HO5sMjo1sL-o;$tDxw zXqFwg=BQ$cjuu;m?c_~m*T?Ay;;o<&H|7eHJLbOg3pg9_cUWTU@(ShTd=@vdN6uta zOCm>W{0!}vzJ0FU%u!tlebSn1?f%g0tTaQ(2wdk+oU~H)_AXC!R(9;JU?ZZBMwbtk-Im%$CgK;bMjuy%mUi*K_i6d@2mnN@XEns=A*u;se0pEHE zwd}?e@Hjs%XsB#mu;>^NqHzD1)BwL>oSRGjl|+FK(J;dBx0KA<%=(n|+n*NGX*Ke; zoI8Fu9d>PX!AKk3j3Z~r70oac&#u>>*HZ$lN;8nwFj?_RMs%k2%xO&zcg$uq;+LrH zQqhX(XCBgkwDAUJY<_KK-0&y)Wo1js&bWlh9DQq0V(EPSH3hYM1)6Lhk%3 z8qfRA5W~BZEv2muV)Z=Y*2ymybMlqBVxiI5fx*m7RX1BkpZ&$l{_lVE_a`N#XjQ6c z>Ua>gD!qa-(1QOxKM#U!_0}emMN+sE8#^;a0Su0doUHqpecvYP?`5|qI_zesznSc> zOIdCtm1bH3-;=W95wm@)C4un8ZY=9PHxGtqe0{H~NQV4fW5_WDCfv2K-qK!bysg)1%SWLn6+n z@Q>Bg@9yiaF5LO71{F=ICF@yUmfL_ow-qwSpoe;_wPs{4iJGlOE`w8`a4y^KV__n5 zCFE&-cJl1HL+J6jpV3;_7@_viv z=~tBP1&ivH0^ad~0|zsHvg`j>GyH$$tG63xDoK5*@mLxx{^xz}n(>+=`?vP=W1Y)> zf&9POPlM^Lz+Oauk*bEh`aVMVT430h9+A!QnykUbFTk{c<4}obgKz0mNZg3(q(bB4A3&Zplh51x1Duta^ zgyx0JxukQc%+3Nia$VWH*`K$c#1g{PN8}}=F?cYi zq2iM2>niY>a)(Z$0`R=r-nU0ZY{`o8F0miRFDUUyrQ`n=Yy zd6U+cmlT3u0@`LHjLrCSmv-G`=qH+X5|?GNE)F?h^XlsA9^*KEQLli%rFr?Wg zgH}7qR_ZNR4W#TJs1!`@vf^mrO^=o)`j?>JP$zjGFriB=e7`wv+?Q%lt_h>^rH_oJ z+WQ|ZE^2%mK8$T#CGWfv7+TTbu^F@iVInPR6sQ{fSkM6!I~cn%gd06H55OTLFj4{* zJ~2K1oDHg|e-fa-XtC0LudrytghpDkl32%2KzkUL!_g1Y9F-IzwYk`Yjhn%i=k&WU zRM@4O9YX0q4aoRKpWn<1E{mdh5*62{m3I)ku@D)$m0AHM^%rp0zFsprGdGt_wl{0s zqA>tTN_MIIEVkY#G^vm`oaz3eoF8p)esn4PvBL1w7zG2ZBPx}4F5%xWnZwN- zw@A8MrH~eP7azG@;wV6JA4j~V+Gd@KxlynH!k1mm&J@+Avb1*QC^VcSm$3ADuG+lX zgmx9awf&0>$VngUy%Ig33)wW+fxEao*p4po=Xv52qRK6_G4nY88^pxMrp>^6BMxJB z58r@KcQcNS+$z`_RqZJxa3=DCA_%X=R98JWlgK4&2p3&NJOR#FxfU>5ZKBYXA#513 z1T75@+?23OOfUsMfCq1KSY*v}En|3^`(vNhc1c zux`sH5;b?SdmDVWYbFEPQ=S80NVU06xbx6l2RDzgQL@vAi*=1ZC!!`Y-D{s=zwoK1 z>H3S@MMWPYIT}Wj8X*!rvdTX6XLv-!z2Fcn!rr?oCl+?w)84??*yAR++1ZE!pcG`Y zdeluZCBRNEvLB_Me1N{i2MTv;cqa78JYAy!u5ZltS|LahmU}hO!4pZ@!ZM)@YWyzX zbgoz+p}jl2N{1K3WK%D$Ni@+&IH6VGW2KCci*7|X7)g4KyX(`$IRMm%alDC44_SeL+I z4IR;|%38r!WhgMu?pQ3S#F<75bG0$vJHlv7UT7}CIaI2wN9veKX3I4zIFLkCYGD98}H4I87f^TN%nyU0&IZ*IFM7ZEJsj zIGz*w*cfV>zoGusT_hAKEt zmZqHONsu(d2ITwGy7kqnSSwN`NNIl1FyeGNduX*BFKiOmr% z9V}el^xkkY3-|3# z_Uju(N@;pIS>hJ9Z%EcyWxJU`KzLIO<`_*5wls=cAK3Q@Uhl)mO4y@&5wJ5qu!)-L5Bq&Bq?cLZE}hdkNyG}&3^|DF^%8(T zPRWyzroZQ__Nml~1S=9%k*wM?_9LrKTE1cG$T$7tBVw63n&H9PcR4TH93~w{AtsmO zrOa2u<}gLbCr!|M97L=L211dw%d!R|-<}Obi_W+zty))m z(=a`>TuQIxcny5O*+he6yZ24z92z0`cyNUz3Wzd>z5ok(Gy4Vs!HpdMy6&Y@+-6vd zoQW6Ab$W^E)8u;*VPIp4c)(@Ojzd%?lAUobJF=E1b-oq|MoZ&AHbnY(EFP zwFZ?SIejnoK;7Zq=X1jF1C*?Ejf+9!b*9J@praZ(D^eKQWy9|~Bb1QZK<$^^V-1Oj z)V6s&oa|d$M=Z*DvOUR!@otj@y5~PbU2A7>A308a?x5FRYJPX&Xs9lQn?LXRf>@@o z1zJ2I3M)kY5&_RYeGBXKa@wBD0Yqn9MSzudK&NdgAL%ZdQV6*K-KjC6>{)v<>l!OH`k6Q7v9YuIPe?az8Ap32X-fq z4>C8t!IDlX+~Qc>J0J3Ry)iVg44KSy%&mI?>bv`CWx0+$j;K%|1eDei<}M=p1JvOz zY|CJ8*gvo*6BC)MeCd?YD8mG_0#kUxurW@=w!11+(Y!i9Mw+%5inLX6*^IjF#ba($ ziDHY`LQs0h7WVrhT6PNp?~8EEUrrbO*^w)~6T&z`r4OCCwLSOl&IP}?>(j0;Nd`V`$Q9w-k7r9I zd}{H{z;t{B_V(;8LKi6n!`*+hb4I)0ih|D~FDpN?b4NAn?ZA@2bU2Sx?CG@VsR9dU z(&TeRjE9wJ<(en_4Xrv7td}EWPUEOZY(m|Q`<-HGVJ zEBQ0>T{w52kjO7S^pzTj!^iXK&0*7VoMu~-I>_Yr!Rq+!p}f!Tt~aaDl3@l?Z__EZ zP2RXUBEfH3-Yc`?_nH+{8NM^pgZyHo_dG^o_RcmZuNHZY20|NakVfW3rPp^pmPu;r zy3H%7wFp|mPe>JDM&m&SVL&Fh3M(%jsgKMe(YJ3M#4@6@tf%}qqhF-Vs|xdvTC2R< z6-+0cXnwUus9H{hK-(3uqL)ilvnLwGFDlgt==o~UTm=axLKiLL�B6@f7a_NIMcoty`4QF-GUoqksEAQ%NV!Tu&OdLgx=+k#NDtu?Q`Xx-pF!gbRp8CBAHwnlr5th5 z-RE0UXGa@dTboQPm`ItEZgp2{P_=$zp1utJ1RJ<8C1hRBI0HghIbbOooZowzx$l$HJa?IdeTNOd2JFwN-jSW3&$Lk0wM~8gCC0Z|R0qN#qtDQxmz36S_-H zY;&w^qpIS@*B2@xOLFkFh#{>oNBpf`DG-K*H-95X-J99u^*Z2-f I`_I4r4J=vbod5s; diff --git a/lib/config-store.js b/lib/config-store.js index 99df744..5881d91 100644 --- a/lib/config-store.js +++ b/lib/config-store.js @@ -56,6 +56,7 @@ const DEFAULTS = { 'clouddrop.cc': { ...HOSTER_SETTINGS_DEFAULTS } }, globalSettings: { + language: 'en', alwaysOnTop: false, shutdownAfterFinish: 'nothing', // nothing | sleep | shutdown | restart logFilePath: '', @@ -73,6 +74,7 @@ const DEFAULTS = { resumeQueueOnLaunch: true, parallelUploadCount: 0, // 0 = use per-hoster limits only scaleParallelUploads: false, + lastBrowseDirectory: '', removeFromQueueOnDone: false, showDropTarget: false, globalMaxSpeedKbs: 0, // 0 = unlimited global speed @@ -575,6 +577,19 @@ class ConfigStore { }, options); } + saveLastBrowseDirectory(directory) { + const snapshot = String(directory || '').trim(); + return this._enqueueWrite(() => { + const current = this.load(); + current.globalSettings = { + ...(current.globalSettings || {}), + lastBrowseDirectory: snapshot + }; + this._guardHosters(current, false); + return this._commit(current); + }); + } + saveRendererGlobalSettings(globalSettings) { const snapshot = this._clone(globalSettings || {}); return this._enqueueWrite(() => { @@ -585,6 +600,7 @@ class ConfigStore { current.globalSettings = { ...snapshot, pendingQueue: currentGlobalSettings.pendingQueue ?? null, + lastBrowseDirectory: currentGlobalSettings.lastBrowseDirectory || '', diagnostics: this._clone(currentGlobalSettings.diagnostics || {}), historyRetention: currentGlobalSettings.historyRetention || 'all', remote: { diff --git a/lib/updater.js b/lib/updater.js index 62745c9..13803b5 100644 --- a/lib/updater.js +++ b/lib/updater.js @@ -6,6 +6,7 @@ const { app } = require('electron'); const UPDATE_REPO = 'Administrator/Multi-Hoster-Upload'; const GITEA_BASE = 'https://git.24-music.de'; const API_URL = `${GITEA_BASE}/api/v1/repos/${UPDATE_REPO}/releases?limit=1`; +const GITHUB_RELEASE_URL = 'https://api.github.com/repos/Sucukdeluxe/Multi-Hoster-Upload/releases/tags'; const CHECK_TIMEOUT = 15000; @@ -86,6 +87,33 @@ async function fetchJson(url, signal) { } } +async function fetchGithubReleaseNotes(remoteVersion, fallback = '', fetchImpl = fetch) { + const version = String(remoteVersion || '').replace(/^v/i, '').trim(); + if (!/^\d+\.\d+\.\d+$/.test(version)) return String(fallback || ''); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT); + try { + const response = await fetchImpl(`${GITHUB_RELEASE_URL}/v${version}`, { + method: 'GET', + redirect: 'follow', + signal: controller.signal, + headers: { + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'Multi-Hoster-Upload' + } + }); + if (!response.ok) return String(fallback || ''); + const release = await response.json(); + const notes = typeof release?.body === 'string' ? release.body.trim() : ''; + return notes || String(fallback || ''); + } catch { + return String(fallback || ''); + } finally { + clearTimeout(timeout); + } +} + async function checkForUpdate() { // Return cached result if fresh if (cachedCheck && (Date.now() - cachedCheckTs) < CACHE_TTL) { @@ -116,6 +144,7 @@ async function checkForUpdate() { return { available: false, reason: 'Kein Setup-Asset im Release gefunden' }; } + const releaseNotes = await fetchGithubReleaseNotes(remoteVersion, release.body || ''); cachedCheck = { available: true, currentVersion, @@ -126,7 +155,7 @@ async function checkForUpdate() { assetSize: setupAsset.size, assetName: setupAsset.name, latestYmlUrl: latestYml ? latestYml.browser_download_url : null, - releaseNotes: release.body || '' + releaseNotes }; cachedCheckTs = Date.now(); return cachedCheck; @@ -293,4 +322,4 @@ function abortUpdate() { } } -module.exports = { checkForUpdate, prepareUpdate, launchPreparedUpdate, abortUpdate, isNewer, resolveReleaseVersion }; +module.exports = { checkForUpdate, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, abortUpdate, isNewer, resolveReleaseVersion }; diff --git a/main.js b/main.js index 118bcb2..ad88be0 100644 --- a/main.js +++ b/main.js @@ -1364,6 +1364,7 @@ function createWindow() { minWidth: 800, minHeight: 550, backgroundColor: '#0f0f0f', + icon: path.join(__dirname, 'assets', 'app_icon.ico'), autoHideMenuBar: true, webPreferences: { contextIsolation: true, @@ -1799,15 +1800,34 @@ async function _dispatchHealthCheck(hoster, hosterConfig, otp) { return { status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' }; } +function getUploadBrowseDirectory() { + const savedDirectory = configStore.load().globalSettings.lastBrowseDirectory; + if (savedDirectory) { + try { + if (fs.statSync(savedDirectory).isDirectory()) return savedDirectory; + } catch {} + } + return app.getPath('downloads'); +} + +async function rememberUploadBrowseDirectory(selectedPath, selectedDirectory = false) { + if (!selectedPath) return; + const directory = selectedDirectory ? selectedPath : path.dirname(selectedPath); + await configStore.saveLastBrowseDirectory(directory); +} + ipcMain.handle('select-files', async () => { const result = await dialog.showOpenDialog(mainWindow, { + defaultPath: getUploadBrowseDirectory(), properties: ['openFile', 'multiSelections'], filters: [ { name: 'Alle Dateien', extensions: ['*'] }, { name: 'Videos', extensions: ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm'] } ] }); - return result.canceled ? null : result.filePaths; + if (result.canceled || !result.filePaths.length) return null; + await rememberUploadBrowseDirectory(result.filePaths[0]); + return result.filePaths; }); // Debug self-test: runs a minimal upload in the main process to verify events work @@ -1837,9 +1857,11 @@ ipcMain.handle('debug-test-upload', async () => { ipcMain.handle('select-folder', async () => { const result = await dialog.showOpenDialog(mainWindow, { + defaultPath: getUploadBrowseDirectory(), properties: ['openDirectory', 'multiSelections'] }); if (result.canceled || !result.filePaths.length) return null; + await rememberUploadBrowseDirectory(result.filePaths[0], true); const files = []; for (const folder of result.filePaths) await walkFolderAsync(folder, files); @@ -1848,9 +1870,11 @@ ipcMain.handle('select-folder', async () => { ipcMain.handle('select-folder-with-sizes', async () => { const result = await dialog.showOpenDialog(mainWindow, { + defaultPath: getUploadBrowseDirectory(), properties: ['openDirectory', 'multiSelections'] }); if (result.canceled || !result.filePaths.length) return null; + await rememberUploadBrowseDirectory(result.filePaths[0], true); const files = []; for (const folder of result.filePaths) await walkFolderAsync(folder, files); diff --git a/package-lock.json b/package-lock.json index e06ebe2..ae64d52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-hoster-uploader", - "version": "2.1.0", + "version": "2.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-hoster-uploader", - "version": "2.1.0", + "version": "2.1.1", "dependencies": { "chokidar": "^3.6.0", "undici": "^7.29.0", diff --git a/package.json b/package.json index 3c4192f..99b24e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "2.1.0", + "version": "2.1.1", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/renderer/app.js b/renderer/app.js index e073dbf..89ea617 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1,4 +1,14 @@ const HOSTERS = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx', 'clouddrop.cc']; +const uiLocalizer = window.I18n.createDomLocalizer(document); +uiLocalizer.start('en'); + +function setUiLanguage(value) { + return uiLocalizer.setLanguage(window.I18n.normalizeLanguage(value)); +} + +function getUiLocale() { + return uiLocalizer.getLanguage() === 'de' ? 'de-DE' : 'en-US'; +} // Dropdown options for "Add Account" modal: value -> label const HOSTER_ADD_OPTIONS = [ @@ -260,6 +270,9 @@ function flushConfigWrites() { let _restoredSnapshotSavedAt = null; let settingsSaveTimer = null; const settingsSaveCoordinator = window.SerializedRunner.createSerializedRunner(performSaveSettings); +let settingsBaseline = ''; +let settingsDirty = false; +let settingsSaving = false; let lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: 0, elapsed: 0, activeJobs: 0 }; const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload'; const QUEUE_COL_WIDTHS_KEY = 'queueColumnWidthsPx'; @@ -342,6 +355,7 @@ window.addEventListener('unhandledrejection', (e) => { // --- Init --- async function init() { config = await window.api.getConfig(); + setUiLanguage(config.globalSettings?.language); hosterSettings = config.hosterSettings || {}; autoHealthCheckEnabled = loadAutoCheckPreference(); ensureAccountStatusEntries(); @@ -527,6 +541,8 @@ function _isHistoryTabActive() { if (nextView) nextView.classList.add('active'); activeTab = tab; syncTabIndicator(tab); + const activeSidebarButton = nextView?.querySelector('.view-sidebar-navigation > .view-sidebar-item.active, .settings-navigation > .settings-nav-button.active'); + _syncSidebarIndicator(activeSidebarButton, true); if (tab.dataset.view === 'history' && (_historyDirty || !_historyEverLoaded)) { loadHistory(); } @@ -3126,7 +3142,7 @@ async function showJobLogModal() { } const fmt = (e) => { - const t = new Date(e.ts || Date.now()).toLocaleTimeString('de-DE', { hour12: false }) + '.' + + const t = new Date(e.ts || Date.now()).toLocaleTimeString(getUiLocale(), { hour12: false }) + '.' + String((e.ts || 0) % 1000).padStart(3, '0'); if (e.kind === 'progress') { const attempt = e.attempt ? ` (${e.attempt}/${e.maxAttempts || '?'})` : ''; @@ -3420,15 +3436,41 @@ function _computeQueueStats() { function _setSidebarCount(id, value) { const element = document.getElementById(id); - if (element) element.textContent = Number(value || 0).toLocaleString('de-DE'); + if (element) element.textContent = Number(value || 0).toLocaleString(getUiLocale()); +} + +function _syncSidebarIndicator(button, immediate = false) { + const navigation = button?.closest('.view-sidebar-navigation, .settings-navigation'); + const indicator = navigation?.querySelector(':scope > .view-sidebar-indicator, :scope > .settings-nav-indicator'); + if (!indicator || !button) return; + const navigationRect = navigation.getBoundingClientRect(); + const buttonRect = button.getBoundingClientRect(); + if (buttonRect.width === 0 || buttonRect.height === 0) return; + const firstPosition = indicator.dataset.ready !== 'true'; + if (immediate || firstPosition) indicator.style.transition = 'none'; + indicator.style.width = `${buttonRect.width}px`; + indicator.style.height = `${buttonRect.height}px`; + indicator.style.transform = `translate(${buttonRect.left - navigationRect.left}px, ${buttonRect.top - navigationRect.top}px)`; + if (immediate || firstPosition) { + indicator.getBoundingClientRect(); + requestAnimationFrame(() => { + indicator.style.transition = ''; + indicator.dataset.ready = 'true'; + }); + } } function _syncSidebarFilterButtons(selector, datasetKey, value) { + let activeButton = null; + let selectionChanged = false; document.querySelectorAll(selector).forEach(button => { const active = button.dataset[datasetKey] === value; + if (active && !button.classList.contains('active')) selectionChanged = true; button.classList.toggle('active', active); button.setAttribute('aria-pressed', active ? 'true' : 'false'); + if (active) activeButton = button; }); + _syncSidebarIndicator(activeButton, !selectionChanged); } function setUploadSidebarFilter(value) { @@ -3490,7 +3532,7 @@ function updateAccountSidebarSummary(allAccounts = getAllAccountsFlat()) { label.textContent = getHosterLabel(name); const count = document.createElement('span'); count.className = 'view-sidebar-badge'; - count.textContent = hosterCounts.get(name).toLocaleString('de-DE'); + count.textContent = hosterCounts.get(name).toLocaleString(getUiLocale()); row.append(dot, label, count); return row; })); @@ -3543,7 +3585,15 @@ function updateHistorySidebarSummary() { _setSidebarCount('historySidebarErrorCount', historySidebarCounts.error); const retention = document.getElementById('historySidebarRetention'); const select = document.getElementById('historyRetentionSelect'); - if (retention && select) retention.textContent = select.selectedOptions[0]?.textContent || 'Alles behalten'; + const labels = { + all: 'Alles behalten', + '7d': 'Letzte 7 Tage', + '30d': 'Letzte 30 Tage', + '90d': 'Letzte 90 Tage', + '1000': 'Letzte 1000 Uploads', + '100': 'Letzte 100 Uploads' + }; + if (retention && select) retention.textContent = labels[select.value] || labels.all; } function updateStatusBar() { @@ -3733,6 +3783,7 @@ function renderSettings() { @@ -3758,6 +3809,24 @@ function renderSettings() { pages.allgemein.innerHTML = ` ${pageHeader('Allgemein', 'Fensterverhalten, Drop-Target und Programmupdates.')} +
+ +
+ + + +
+ +
@@ -3769,8 +3838,11 @@ function renderSettings() {
-
- +
+
+ Nach neuer Version suchen + Verfügbare Updates werden zusammen mit dem Changelog angezeigt. +
`; @@ -3898,7 +3970,7 @@ function renderSettings() { pages.logs.innerHTML = ` ${pageHeader('Logs & Support', 'Protokollierung verwalten, Log-Dateien öffnen und ein bereinigtes Support-Paket erstellen.')} -
+
@@ -4047,6 +4119,8 @@ function renderSettings() { const activateSettingsPage = (target, focus = false) => { const activeButton = navigation.querySelector(`[data-settings-page="${target}"]`); if (!activeButton || activeButton.hidden) return; + const indicator = navigation.querySelector('.settings-nav-indicator'); + if (indicator) indicator.hidden = false; navigation.querySelectorAll('.settings-nav-button').forEach((button) => { const active = button === activeButton; button.classList.toggle('active', active); @@ -4055,6 +4129,7 @@ function renderSettings() { Object.values(pages).forEach((page) => { page.classList.toggle('active', page.dataset.subpage === target); }); + _syncSidebarIndicator(activeButton); if (focus) activeButton.focus(); content.scrollTop = 0; }; @@ -4081,7 +4156,7 @@ function renderSettings() { const searchInput = layout.querySelector('#settingsSearchInput'); const searchEmpty = layout.querySelector('#settingsSearchEmpty'); searchInput.addEventListener('input', () => { - const query = searchInput.value.trim().toLocaleLowerCase('de-DE'); + const query = searchInput.value.trim().toLocaleLowerCase(getUiLocale()); const visibleButtons = []; navigation.querySelectorAll('.settings-nav-button').forEach((button) => { const visible = !query || button.dataset.search.includes(query); @@ -4092,10 +4167,14 @@ function renderSettings() { const activeButton = navigation.querySelector('.settings-nav-button.active'); if (visibleButtons.length === 0) { Object.values(pages).forEach((page) => page.classList.remove('active')); + const indicator = navigation.querySelector('.settings-nav-indicator'); + if (indicator) indicator.hidden = true; return; } if (!activeButton || activeButton.hidden || !content.querySelector('.settings-subpage.active')) { activateSettingsPage((activeButton && !activeButton.hidden ? activeButton : visibleButtons[0]).dataset.settingsPage); + } else { + _syncSidebarIndicator(activeButton, true); } }); @@ -4218,7 +4297,7 @@ function renderSettings() { const fmtIssued = (ts) => { if (!ts) return ''; - try { return 'Code erstellt: ' + new Date(ts).toLocaleString('de-DE'); } catch { return ''; } + try { return 'Code erstellt: ' + new Date(ts).toLocaleString(getUiLocale()); } catch { return ''; } }; const parseAllowlist = () => allowlistEl.value.split(/\r?\n/).map((l) => l.trim()).filter(Boolean); const renderModeUi = (suggestedHosts) => { @@ -4264,7 +4343,7 @@ function renderSettings() { const el = document.getElementById('diagConnectionStatus'); if (!el || !st) return; if (st.running) { - const last = st.lastAccess ? new Date(st.lastAccess).toLocaleString('de-DE') : '—'; + const last = st.lastAccess ? new Date(st.lastAccess).toLocaleString(getUiLocale()) : '—'; const scope = st.bindMode === 'network' ? `Netzwerk (Allowlist: ${st.allowlistCount})` : 'nur lokal'; el.textContent = `Aktiv auf ${st.bindAddress}:${st.port} (${scope}) — ${st.clientCount} Client(s) — Letzter Zugriff: ${last}`; el.style.color = '#10b981'; @@ -4336,8 +4415,37 @@ function renderSettings() { document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', requestUpdateCheck); _syncHeaderUpdateState(); container.querySelectorAll('.settings-autosave').forEach((input) => { - const eventName = input.type === 'checkbox' ? 'change' : 'input'; - input.addEventListener(eventName, scheduleSettingsSave); + const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input'; + input.addEventListener(eventName, () => { + if (input.id === 'languageInput') { + setUiLanguage(input.value); + syncLanguagePicker(input.value); + } + markSettingsDirty(); + }); + }); + container.querySelectorAll('.language-option').forEach(button => { + button.addEventListener('click', () => { + const input = document.getElementById('languageInput'); + if (!input || input.value === button.dataset.language) return; + input.value = button.dataset.language; + input.dispatchEvent(new window.Event('change', { bubbles: true })); + }); + }); + syncLanguagePicker(globalSettings.language); + establishSettingsBaseline(); + window.requestAnimationFrame(() => { + if (layout.isConnected) _syncSidebarIndicator(navigation.querySelector('.settings-nav-button.active'), true); + }); +} + +function syncLanguagePicker(value) { + const language = window.I18n.normalizeLanguage(value); + const picker = document.getElementById('languagePicker'); + if (!picker) return; + picker.dataset.language = language; + picker.querySelectorAll('.language-option').forEach(button => { + button.setAttribute('aria-pressed', String(button.dataset.language === language)); }); } @@ -4346,24 +4454,66 @@ async function chooseLogFilePath() { if (!folders || !folders[0]) return; const normalized = folders[0].replace(/[\\\/]+$/, ''); document.getElementById('logFilePathInput').value = `${normalized}\\fileuploader.log`; - scheduleSettingsSave(); + markSettingsDirty(); +} + +function captureSettingsState() { + const controls = Array.from(document.querySelectorAll('#settings-view .settings-autosave')); + return JSON.stringify(controls.map(control => [ + control.id || control.name || '', + control.type === 'checkbox' ? control.checked : control.value + ])); +} + +function syncSettingsSaveState(message) { + const button = document.getElementById('saveSettingsBtn'); + const feedback = document.getElementById('saveFeedback'); + if (button) { + button.disabled = settingsSaving || !settingsDirty; + button.classList.toggle('btn-success', settingsDirty && !settingsSaving); + button.classList.toggle('btn-secondary', !settingsDirty || settingsSaving); + } + if (feedback && message) feedback.textContent = message; +} + +function establishSettingsBaseline(message = 'Keine ungespeicherten Änderungen') { + settingsBaseline = captureSettingsState(); + settingsDirty = false; + syncSettingsSaveState(message); +} + +function markSettingsDirty() { + settingsDirty = captureSettingsState() !== settingsBaseline; + syncSettingsSaveState(settingsDirty ? 'Ungespeicherte Änderungen' : 'Keine ungespeicherten Änderungen'); } function scheduleSettingsSave() { - if (closePreparationState !== 'open') return; - const feedback = document.getElementById('saveFeedback'); - if (feedback) feedback.textContent = 'Speichert...'; - clearTimeout(settingsSaveTimer); - settingsSaveTimer = setTimeout(() => { - settingsSaveTimer = null; - saveSettings({ feedbackText: 'Automatisch gespeichert' }).catch((err) => { - if (feedback) feedback.textContent = `Speichern fehlgeschlagen: ${err.message}`; - }); - }, 350); + markSettingsDirty(); } function saveSettings(options = {}) { - return settingsSaveCoordinator.run(options); + const requestedState = captureSettingsState(); + settingsSaving = true; + syncSettingsSaveState('Speichert…'); + return settingsSaveCoordinator.run(options).then(result => { + const savedMessage = options.feedbackText || 'Gespeichert!'; + settingsBaseline = requestedState; + settingsDirty = captureSettingsState() !== settingsBaseline; + syncSettingsSaveState(settingsDirty ? 'Ungespeicherte Änderungen' : savedMessage); + setTimeout(() => { + const feedback = document.getElementById('saveFeedback'); + if (!settingsDirty && feedback?.textContent === savedMessage) syncSettingsSaveState('Keine ungespeicherten Änderungen'); + }, 1800); + return result; + }, error => { + settingsSaving = false; + settingsDirty = captureSettingsState() !== settingsBaseline; + syncSettingsSaveState(`Speichern fehlgeschlagen: ${error.message}`); + throw error; + }).finally(() => { + settingsSaving = false; + syncSettingsSaveState(); + }); } async function performSaveSettings(options = {}) { @@ -4383,6 +4533,10 @@ async function performSaveSettings(options = {}) { const globalSettings = { ...cur, + language: (() => { + const el = document.getElementById('languageInput'); + return window.I18n.normalizeLanguage(el ? el.value : cur.language); + })(), logFilePath: elTxt('logFilePathInput', cur.logFilePath || '').trim(), logMode: (() => { const el = document.getElementById('logModeInput'); @@ -4519,12 +4673,7 @@ async function performSaveSettings(options = {}) { } const feedback = document.getElementById('saveFeedback'); - feedback.textContent = feedbackText; - setTimeout(() => { - if (feedback.textContent === feedbackText) { - feedback.textContent = 'Änderungen werden automatisch gespeichert.'; - } - }, 1800); + if (feedback) feedback.textContent = feedbackText; } // --- Accounts --- @@ -5489,6 +5638,112 @@ function _hideOtpField() { } // --- History --- +let historyRetentionMenuToken = 0; + +function syncHistoryRetentionPicker() { + const select = document.getElementById('historyRetentionSelect'); + const value = document.getElementById('historyRetentionValue'); + const menu = document.getElementById('historyRetentionMenu'); + if (!select || !value || !menu) return; + const labels = { + all: 'Alles behalten', + '7d': 'Letzte 7 Tage', + '30d': 'Letzte 30 Tage', + '90d': 'Letzte 90 Tage', + '1000': 'Letzte 1000 Uploads', + '100': 'Letzte 100 Uploads' + }; + value.textContent = labels[select.value] || labels.all; + menu.querySelectorAll('[data-history-retention]').forEach(option => { + const selected = option.dataset.historyRetention === select.value; + option.setAttribute('aria-selected', String(selected)); + option.tabIndex = selected ? 0 : -1; + }); +} + +function openHistoryRetentionMenu(focusOption = false) { + const trigger = document.getElementById('historyRetentionTrigger'); + const menu = document.getElementById('historyRetentionMenu'); + if (!trigger || !menu) return; + historyRetentionMenuToken++; + menu.classList.remove('menu-closing', 'menu-opening'); + menu.style.display = 'block'; + void menu.offsetHeight; + menu.classList.add('menu-opening'); + trigger.setAttribute('aria-expanded', 'true'); + if (focusOption) { + window.requestAnimationFrame(() => menu.querySelector('[aria-selected="true"]')?.focus()); + } +} + +function closeHistoryRetentionMenu(returnFocus = false) { + const trigger = document.getElementById('historyRetentionTrigger'); + const menu = document.getElementById('historyRetentionMenu'); + if (!trigger || !menu || window.getComputedStyle(menu).display === 'none') return; + const token = ++historyRetentionMenuToken; + menu.classList.remove('menu-opening', 'menu-closing'); + void menu.offsetHeight; + menu.classList.add('menu-closing'); + trigger.setAttribute('aria-expanded', 'false'); + const finish = () => { + if (!Object.is(historyRetentionMenuToken, token)) return; + menu.style.display = 'none'; + menu.classList.remove('menu-closing'); + if (returnFocus) trigger.focus(); + }; + menu.addEventListener('animationend', finish, { once: true }); + window.setTimeout(finish, 220); +} + +function selectHistoryRetentionOption(value) { + const select = document.getElementById('historyRetentionSelect'); + if (!select || !select.querySelector(`option[value="${value}"]`)) return; + select.value = value; + syncHistoryRetentionPicker(); + closeHistoryRetentionMenu(true); + select.dispatchEvent(new window.Event('change', { bubbles: true })); +} + +function syncHistoryClearAction() { + const button = document.getElementById('clearHistoryBtn'); + if (button) button.disabled = historyRowsData.length === 0; +} + +function closeHistoryClearModal() { + const modal = document.getElementById('historyClearModal'); + if (!modal) return; + modal.style.display = 'none'; + modal.setAttribute('aria-hidden', 'true'); + document.getElementById('clearHistoryBtn')?.focus(); +} + +function openHistoryClearModal() { + const button = document.getElementById('clearHistoryBtn'); + const modal = document.getElementById('historyClearModal'); + if (!modal || !button || button.disabled) return; + modal.style.display = 'flex'; + modal.setAttribute('aria-hidden', 'false'); + document.getElementById('confirmHistoryClearBtn')?.focus(); +} + +async function confirmHistoryClear() { + const confirmButton = document.getElementById('confirmHistoryClearBtn'); + const cancelButton = document.getElementById('cancelHistoryClearBtn'); + if (!confirmButton || confirmButton.disabled) return; + confirmButton.disabled = true; + cancelButton.disabled = true; + try { + await runConfigWrite(() => window.api.clearHistory()); + await loadHistory(); + closeHistoryClearModal(); + } catch (error) { + showCopyToast(error.message || String(error)); + } finally { + confirmButton.disabled = false; + cancelButton.disabled = false; + } +} + async function loadHistory() { const history = await window.api.getHistory(); window._historyForStats = history || []; @@ -5496,13 +5751,17 @@ async function loadHistory() { _historyDirty = false; _invalidateHosterLifetimeCache(); const retSel = document.getElementById('historyRetentionSelect'); - if (retSel) retSel.value = (config.globalSettings && config.globalSettings.historyRetention) || 'all'; + if (retSel) { + retSel.value = (config.globalSettings && config.globalSettings.historyRetention) || 'all'; + syncHistoryRetentionPicker(); + } const container = document.getElementById('historyContainer'); if (!history || history.length === 0) { historyRowsData = []; historySidebarCounts = { total: 0, success: 0, error: 0 }; updateHistorySidebarSummary(); + syncHistoryClearAction(); container.innerHTML = '

Noch keine Uploads.

'; return; } @@ -5534,6 +5793,7 @@ async function loadHistory() { } updateHistorySidebarSummary(); + syncHistoryClearAction(); renderHistoryTable(container); } @@ -5784,7 +6044,7 @@ function renderHistoryTable(container) { if (notice) { if (total > HISTORY_RENDER_CAP) { notice.style.display = ''; - notice.textContent = `Zeige neueste ${HISTORY_RENDER_CAP.toLocaleString('de-DE')} von ${total.toLocaleString('de-DE')} Einträgen. Der vollständige Verlauf bleibt gespeichert und ist über „Verlauf exportieren“ verfügbar.`; + notice.textContent = `Zeige neueste ${HISTORY_RENDER_CAP.toLocaleString(getUiLocale())} von ${total.toLocaleString(getUiLocale())} Einträgen. Der vollständige Verlauf bleibt gespeichert und ist über „Verlauf exportieren“ verfügbar.`; } else { notice.style.display = 'none'; } @@ -5970,6 +6230,15 @@ function setupListeners() { _syncSidebarFilterButtons('[data-upload-sidebar-target]', 'uploadSidebarTarget', uploadSidebarFilter); _syncSidebarFilterButtons('[data-accounts-sidebar-filter]', 'accountsSidebarFilter', accountSidebarFilter); _syncSidebarFilterButtons('[data-history-filter]', 'historyFilter', historySidebarFilter); + let sidebarResizeFrame = 0; + window.addEventListener('resize', () => { + window.cancelAnimationFrame(sidebarResizeFrame); + sidebarResizeFrame = window.requestAnimationFrame(() => { + document.querySelectorAll('.view.active .view-sidebar-navigation > .view-sidebar-item.active, .view.active .settings-navigation > .settings-nav-button.active').forEach(button => { + _syncSidebarIndicator(button, true); + }); + }); + }); document.getElementById('addFilesBtn').addEventListener('click', pickFiles); document.getElementById('addFolderBtn').addEventListener('click', pickFolder); document.getElementById('startUploadBtn').addEventListener('click', startUpload); @@ -6064,16 +6333,73 @@ function setupListeners() { } }, true); - document.getElementById('clearHistoryBtn').addEventListener('click', async () => { - if (!confirm('Verlauf wirklich löschen?')) return; - try { - await runConfigWrite(() => window.api.clearHistory()); - loadHistory(); - } catch (error) { - showCopyToast(error.message || String(error)); + document.getElementById('clearHistoryBtn').addEventListener('click', openHistoryClearModal); + document.getElementById('confirmHistoryClearBtn').addEventListener('click', confirmHistoryClear); + document.getElementById('cancelHistoryClearBtn').addEventListener('click', closeHistoryClearModal); + document.getElementById('closeHistoryClearModalBtn').addEventListener('click', closeHistoryClearModal); + document.getElementById('historyClearModal').addEventListener('click', event => { + if (event.target.id === 'historyClearModal') closeHistoryClearModal(); + }); + document.addEventListener('keydown', event => { + const modal = document.getElementById('historyClearModal'); + if (modal?.style.display !== 'flex' || event.key !== 'Escape') return; + event.preventDefault(); + closeHistoryClearModal(); + }, true); + document.getElementById('exportHistoryBtn').addEventListener('click', exportHistory); + + const historyRetentionPicker = document.getElementById('historyRetentionPicker'); + const historyRetentionTrigger = document.getElementById('historyRetentionTrigger'); + const historyRetentionMenu = document.getElementById('historyRetentionMenu'); + historyRetentionTrigger.addEventListener('click', event => { + event.stopPropagation(); + if (window.getComputedStyle(historyRetentionMenu).display === 'none' || historyRetentionMenu.classList.contains('menu-closing')) { + openHistoryRetentionMenu(); + } else { + closeHistoryRetentionMenu(); } }); - document.getElementById('exportHistoryBtn').addEventListener('click', exportHistory); + historyRetentionTrigger.addEventListener('keydown', event => { + if (!['ArrowDown', 'ArrowUp'].includes(event.key)) return; + event.preventDefault(); + openHistoryRetentionMenu(true); + }); + historyRetentionMenu.addEventListener('click', event => { + const option = event.target.closest('[data-history-retention]'); + if (option) selectHistoryRetentionOption(option.dataset.historyRetention); + }); + historyRetentionMenu.addEventListener('keydown', event => { + const options = [...historyRetentionMenu.querySelectorAll('[data-history-retention]')]; + const current = options.indexOf(document.activeElement); + if (event.key === 'Escape') { + event.preventDefault(); + closeHistoryRetentionMenu(true); + return; + } + if (event.key === 'Enter' || event.key === ' ') { + const option = event.target.closest('[data-history-retention]'); + if (!option) return; + event.preventDefault(); + selectHistoryRetentionOption(option.dataset.historyRetention); + return; + } + if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return; + event.preventDefault(); + const next = event.key === 'Home' + ? 0 + : event.key === 'End' + ? options.length - 1 + : (Math.max(0, current) + (event.key === 'ArrowDown' ? 1 : -1) + options.length) % options.length; + options[next]?.focus(); + }); + historyRetentionPicker.addEventListener('focusout', () => { + window.setTimeout(() => { + if (!historyRetentionPicker.contains(document.activeElement)) closeHistoryRetentionMenu(); + }, 0); + }); + document.addEventListener('mousedown', event => { + if (!historyRetentionPicker.contains(event.target)) closeHistoryRetentionMenu(); + }); const historyRetentionSelect = document.getElementById('historyRetentionSelect'); if (historyRetentionSelect) { @@ -6083,17 +6409,22 @@ function setupListeners() { if (value !== 'all') { const preview = await window.api.pruneHistory(value, { dryRun: true }); if (preview && preview.removedRows > 0) { - const ok = confirm(`${preview.removedRows.toLocaleString('de-DE')} Verlaufseinträge werden dauerhaft entfernt.\n\nFortfahren?`); - if (!ok) { historyRetentionSelect.value = prev; return; } + const ok = confirm(`${preview.removedRows.toLocaleString(getUiLocale())} Verlaufseinträge werden dauerhaft entfernt.\n\nFortfahren?`); + if (!ok) { + historyRetentionSelect.value = prev; + syncHistoryRetentionPicker(); + return; + } } } try { const res = await runConfigWrite(() => window.api.pruneHistory(value)); config.globalSettings = { ...(config.globalSettings || {}), historyRetention: value }; - if (res && res.removedRows > 0) showCopyToast(`Verlauf gekürzt: ${res.removedRows.toLocaleString('de-DE')} entfernt`); + if (res && res.removedRows > 0) showCopyToast(`Verlauf gekürzt: ${res.removedRows.toLocaleString(getUiLocale())} entfernt`); loadHistory(); } catch (error) { historyRetentionSelect.value = prev; + syncHistoryRetentionPicker(); showCopyToast(error.message || String(error)); } }); @@ -6227,17 +6558,18 @@ function showUpdateBanner(info) { const title = document.getElementById('updateDialogTitle'); const message = document.getElementById('updateMessage'); const notes = document.getElementById('updateReleaseNotes'); + const notesBody = document.getElementById('updateReleaseNotesBody'); const installButton = document.getElementById('installUpdateBtn'); if (title) title.textContent = 'Eine neue Version ist verfügbar'; if (message) message.textContent = `Update v${version} verfügbar`; - if (notes) { + if (notes && notesBody) { const releaseNotes = String(info.releaseNotes || '').trim(); - notes.textContent = releaseNotes.length > 2400 ? `${releaseNotes.slice(0, 2399)}…` : releaseNotes; + notesBody.textContent = releaseNotes.length > 2400 ? `${releaseNotes.slice(0, 2399)}…` : releaseNotes; notes.hidden = !releaseNotes; } if (installButton) { installButton.disabled = false; - installButton.textContent = 'Jetzt updaten'; + installButton.textContent = 'Jetzt installieren'; } _setUpdateProgress(0, 'Bereit zum Download'); _setUpdateDialogBusy(false); @@ -6583,8 +6915,8 @@ function formatDateTime(value) { const safeDate = Number.isNaN(date.getTime()) ? new Date() : date; return { ts: safeDate.getTime(), - text: safeDate.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) - + ' ' + safeDate.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) + text: safeDate.toLocaleDateString(getUiLocale(), { day: '2-digit', month: '2-digit', year: 'numeric' }) + + ' ' + safeDate.toLocaleTimeString(getUiLocale(), { hour: '2-digit', minute: '2-digit', second: '2-digit' }) }; } diff --git a/renderer/i18n.js b/renderer/i18n.js new file mode 100644 index 0000000..b41039b --- /dev/null +++ b/renderer/i18n.js @@ -0,0 +1,587 @@ +(function (root, factory) { + const api = factory(); + if (typeof module === 'object' && module.exports) module.exports = api; + if (root) root.I18n = api; +})(typeof window !== 'undefined' ? window : globalThis, function () { + const pairs = [ + ['Arbeitsbereich', 'Workspace'], + ['Accounts verwalten', 'Manage accounts'], + ['Archiv', 'Archive'], + ['(aktivieren zum Erzeugen)', '(enable to generate)'], + ['-Tab direkt beim jeweiligen Hoster.', ' tab for the relevant host.'], + ['127.0.0.1 oder Tunnel-/Tailscale-Adresse', '127.0.0.1 or tunnel/Tailscale address'], + ['API-Token', 'API token'], + ['Account-Status', 'Account status'], + ['Allowlist (IP/CIDR, eine pro Zeile)', 'Allowlist (one IP/CIDR per line)'], + ['Aufbewahrung', 'Retention'], + ['Ausgabe-Format der kopierten Links', 'Output format for copied links'], + ['Auto-Check vor Upload', 'Auto-check before upload'], + ['Automatischer Check vor dem Upload', 'Automatic check before upload'], + ['Bildschirm und Eingabesteuerung bleiben gesperrt.', 'Screen and input control remain locked.'], + ['Bindet nur an', 'Binds only to'], + ['Bindet an', 'Binds to'], + ['. Nur IPs/CIDRs aus der Allowlist dürfen verbinden (Loopback immer) — zusätzlich zum Token. Über Tailscale: trage deinen Tailnet-Bereich ein (z.B.', '. Only IPs/CIDRs from the allowlist may connect (loopback is always allowed), in addition to the token. For Tailscale, enter your tailnet range, e.g.'], + [') und die Tailscale-IP/MagicDNS oben als Code-Adresse. Transport ist plaintext über den Tunnel — Tailscale/WireGuard verschlüsselt.', ') and enter the Tailscale IP or MagicDNS name above as the code address. Transport is plaintext through the tunnel; Tailscale/WireGuard provides encryption.'], + ['Datum', 'Date'], + ['Einstellungskategorien', 'Settings categories'], + ['Erlaubt', 'Allowed'], + ['Erneut versuchen', 'Retry'], + ['Exportieren', 'Export'], + ['Fehlgeschlagene erneut', 'Retry failed'], + ['Fensterverhalten, Drop-Target und Programmupdates.', 'Window behavior, drop target, and application updates.'], + ['Gesamt:', 'Total:'], + ['Globales Speed-Limit', 'Global speed limit'], + ['Hauptbereiche', 'Main sections'], + ['Hinweis', 'Notice'], + ['Hoster-Limits automatisch hochskalieren', 'Automatically scale host limits'], + ['Hoster', 'Host'], + ['Importieren', 'Import'], + ['In Zwischenablage', 'To clipboard'], + ['In diesem Lauf hochgeladen:', 'Uploaded during this run:'], + ['Input erlauben', 'Allow input'], + ['Installierte Version', 'Installed version'], + ['Laufzeit:', 'Runtime:'], + ['Letzte 100 Uploads', 'Last 100 uploads'], + ['Letzte 1000 Uploads', 'Last 1,000 uploads'], + ['Letzte 7 Tage', 'Last 7 days'], + ['Letzte 30 Tage', 'Last 30 days'], + ['Letzte 90 Tage', 'Last 90 days'], + ['Max. parallele Uploads', 'Max. parallel uploads'], + ['Maximale parallele Uploads', 'Maximum parallel uploads'], + ['Neustart', 'Restart'], + ['Noch keine Accounts', 'No accounts yet'], + ['Nur lokal (127.0.0.1) — Tunnel/VPN', 'Local only (127.0.0.1) — tunnel/VPN'], + ['Parallele Uploads verringern', 'Decrease parallel uploads'], + ['Restzeit:', 'Time remaining:'], + ['Rest', 'Remaining'], + ['Schnell finden', 'Quick find'], + ['Schnelleinstellungen', 'Quick settings'], + ['Sicherung', 'Backup'], + ['Sichtbarkeit', 'Visibility'], + ['Sitzung', 'Session'], + ['Speicherstatus', 'Save status'], + ['Statistik', 'Statistics'], + ['Suche Aktualisierungen', 'Checking for updates'], + ['Verbleibend:', 'Remaining:'], + ['Zuletzt erzeugte Upload-Links', 'Recently generated upload links'], + ['User-ID, role:ROLLEN-ID, @here oder @everyone', 'User ID, role:ROLE-ID, @here, or @everyone'], + ['https://discord.com/api/webhooks/… oder eigene URL', 'https://discord.com/api/webhooks/… or custom URL'], + ['nur lesenden', 'read-only'], + ['z. B. Hauptaccount, Premium, Kunde XY', 'e.g. primary account, premium, customer XY'], + ['Datei', 'File'], + ['Dateien', 'Files'], + ['Dateien hinzufügen', 'Add files'], + ['Ordner hinzufügen', 'Add folder'], + ['Online-Schlüssel erstellen', 'Create online key'], + ['Online-Schlüssel importieren', 'Import online key'], + ['Einstellungen öffnen', 'Open settings'], + ['Geschwindigkeitslimit', 'Speed limit'], + ['Hilfe', 'Help'], + ['Log-Ordner öffnen', 'Open log folder'], + ['Diagnose-Paket exportieren', 'Export diagnostics package'], + ['Eine neue Version ist verfügbar', 'A new version is available'], + ['Bereit zum Download', 'Ready to download'], + ['Abbrechen', 'Cancel'], + ['Jetzt installieren', 'Install now'], + ['Jetzt updaten', 'Install now'], + ['Alle Dateien', 'All files'], + ['Aktiv', 'Active'], + ['Warteschlange', 'Queue'], + ['Fertig', 'Completed'], + ['Fehler', 'Failed'], + ['Verfügbarkeit', 'Availability'], + ['Bereite Accounts', 'Ready accounts'], + ['Primär', 'Primary'], + ['Nicht geprüft', 'Not checked'], + ['Nicht aktiv', 'Inactive'], + ['Prüfen', 'Check'], + ['Bearbeiten', 'Edit'], + ['Keine Upload-Ziele ausgewählt', 'No upload destinations selected'], + ['Dateien ablegen, Ziele wählen und Uploads zentral steuern.', 'Drop files, choose destinations, and manage uploads in one place.'], + ['Upload-Aufträge', 'Upload jobs'], + ['Dateien hinzufügen, Ziele auswählen und Fortschritt verfolgen', 'Add files, choose destinations, and track progress'], + ['+ Dateien', '+ Files'], + ['+ Ordner', '+ Folder'], + ['Dateien hierher ziehen oder klicken', 'Drop files here or click to browse'], + ['Dateien und Ordner werden vor dem Upload geprüft.', 'Files and folders are checked before upload.'], + ['Dateiname', 'File name'], + ['Hoster / Account', 'Host / Account'], + ['Hochgeladen / Größe', 'Uploaded / Size'], + ['Geschwindigkeit', 'Speed'], + ['Fortschritt', 'Progress'], + ['Status', 'Status'], + ['Aktionen', 'Actions'], + ['Alle Links kopieren', 'Copy all links'], + ['Alle entfernen', 'Remove all'], + ['Entfernen', 'Remove'], + ['Dateien in der Warteschlange', 'Files in queue'], + ['Läuft:', 'Running:'], + ['Fehler:', 'Failed:'], + ['Dateigröße in der Warteschlange', 'Queued file size'], + ['Upload-Geschwindigkeit:', 'Upload speed:'], + ['Zugänge', 'Accounts'], + ['Alle Accounts', 'All accounts'], + ['Bereit', 'Ready'], + ['Aktion nötig', 'Action required'], + ['Zugänge prüfen, priorisieren und für Uploads bereitstellen.', 'Check, prioritize, and prepare accounts for uploads.'], + ['Hoster-Zugangsdaten verwalten und prüfen', 'Manage and verify host account credentials'], + ['Accounts prüfen', 'Check accounts'], + ['Account hinzufügen', 'Add account'], + ['Füge deinen ersten Hoster-Account hinzu. Die Zugangsdaten werden vor dem Speichern geprüft.', 'Add your first host account. Credentials are verified before saving.'], + ['Alle ausklappen', 'Expand all'], + ['Alle einklappen', 'Collapse all'], + ['Wähle einen Hoster und gib deine Zugangsdaten ein.', 'Choose a host and enter your credentials.'], + ['Wähle einen Hoster und gib deine Zugangsdaten ein. Der Account wird vor dem Anlegen geprüft.', 'Choose a host and enter your credentials. The account is checked before it is added.'], + ['Prüfen und anlegen', 'Verify and add'], + ['Prüfen und speichern', 'Verify and save'], + ['Keine Einträge.', 'No entries.'], + ['Schließen', 'Close'], + ['Account löschen?', 'Delete account?'], + ['Account wirklich löschen?', 'Are you sure you want to delete this account?'], + ['Löschen', 'Delete'], + ['Alle Optionen nach Aufgaben sortiert. Änderungen werden mit dem Speichern-Button übernommen.', 'All options are grouped by task. Changes are applied with the Save button.'], + ['Speichern', 'Save'], + ['Einstellungen', 'Settings'], + ['Allgemein', 'General'], + ['Uploads', 'Uploads'], + ['Automatik', 'Automation'], + ['Benachrichtigungen', 'Notifications'], + ['Logs & Support', 'Logs & Support'], + ['Fernsteuerung', 'Remote control'], + ['Diagnose-Zugriff', 'Diagnostics access'], + ['Backup & Übertragen', 'Backup & Transfer'], + ['Keine passende Einstellung gefunden.', 'No matching setting found.'], + ['Keine ungespeicherten Änderungen', 'No unsaved changes'], + ['Oberfläche', 'Interface'], + ['Sprache', 'Language'], + ['Englisch', 'English'], + ['Deutsch', 'German'], + ['Immer im Vordergrund', 'Always on top'], + ['Drop-Target anzeigen', 'Show drop target'], + ['Programmupdate', 'Application update'], + ['Nach neuer Version suchen', 'Check for a newer version'], + ['Verfügbare Updates werden zusammen mit dem Changelog angezeigt.', 'Available updates are shown together with the changelog.'], + ['Neue Version suchen', 'Check for a new version'], + ['Nach Updates suchen', 'Check for updates'], + ['Upload-Verhalten', 'Upload behavior'], + ['Globale Leistung, Warteschlange und Verhalten nach einem erfolgreichen Upload.', 'Global performance, queue, and behavior after a successful upload.'], + ['Leistung', 'Performance'], + ['Globale parallele Uploads', 'Global parallel uploads'], + ['0 = nur die Einstellung des jeweiligen Hosters verwenden', '0 = use the setting of each host'], + ['Verteilt die globale Parallelität auf vorhandene Accounts eines Hosters.', 'Distributes global parallelism across the available accounts of a host.'], + ['Nach Abschluss', 'After completion'], + ['Nach Abschluss aus der Liste entfernen', 'Remove from the list after completion'], + ['Erfolgreich hochgeladene Dateien verschwinden automatisch aus der Upload-Liste.', 'Successfully uploaded files are automatically removed from the upload list.'], + ['Warteschlange beim Start wiederherstellen', 'Restore queue on startup'], + ['Noch nicht abgeschlossene Uploads werden beim nächsten Programmstart erneut angezeigt.', 'Incomplete uploads are shown again the next time the application starts.'], + ['Hoster-Einstellungen', 'Host settings'], + ['Upload-Einstellungen', 'Upload settings'], + ['Erfolgreiche Links in fileuploader.log.', 'Successful links in fileuploader.log.'], + ['Verteilt die Dateien reihum auf alle aktiven Accounts dieses Hosters (Datei 1 → Account 1, Datei 2 → Account 2 …). Hält z. B. byse-Accounts aktiv. Nur ein Account = kein Effekt.', 'Distributes files across all active accounts for this host in round-robin order (file 1 → account 1, file 2 → account 2, and so on). Keeps accounts such as byse active. Has no effect with only one account.'], + ['Größen-Limit merken', 'Remember size limit'], + ['Überspringt nach zwei verdächtigen Ablehnungen auf einem Account größere Dateien dort vorab ("Bekanntes Größen-Limit"). Abschalten = jede Datei wird immer wirklich versucht.', 'After two suspicious rejections on an account, larger files are skipped there in advance ("Known size limit"). When disabled, every file is always attempted.'], + ['Einstellungen einzelner Hoster', 'Settings for individual hosts'], + ['wie Wiederholungen, Geschwindigkeit, Parallelität, Dateigröße und Logging findest du im', 'such as retries, speed, parallelism, file size, and logging are available in'], + ['Accounts-Tab.', 'the Accounts tab.'], + ['Wiederholungen und überwachte Ordner für unbeaufsichtigte Uploads.', 'Retries and watched folders for unattended uploads.'], + ['Unbeaufsichtigter Betrieb', 'Unattended operation'], + ['Automatische Wiederholungsrunden', 'Automatic retry rounds'], + ['0 = aus. Nach Batch-Ende werden transiente Fehler (Netzwerk, Hoster-Flake) automatisch bis zu N Runden neu versucht.', '0 = off. After a batch ends, transient errors (network or host issues) are retried automatically for up to N rounds.'], + ['Wartezeit zwischen Runden', 'Delay between rounds'], + ['Minuten · jede weitere Runde wartet entsprechend länger', 'Minutes · each additional round waits proportionally longer'], + ['Ordnerüberwachung', 'Folder monitoring'], + ['Inaktiv', 'Inactive'], + ['Ordnerpfad', 'Folder path'], + ['Wählen', 'Choose'], + ['Dateierweiterungen', 'File extensions'], + ['Nur diese', 'Only these'], + ['Alle außer', 'All except'], + ['Verzögerung (Sekunden)', 'Delay (seconds)'], + ['Warten bis Datei fertig geschrieben', 'Wait until the file has finished writing'], + ['Verhalten', 'Behavior'], + ['Aktiviert', 'Enabled'], + ['Unterordner einbeziehen', 'Include subfolders'], + ['Duplikate überspringen', 'Skip duplicates'], + ['Auto-Upload starten', 'Start uploads automatically'], + ['Hoster-Vorauswahl', 'Host preselection'], + ['Erst Accounts anlegen, dann hier auswählen.', 'Add accounts first, then select them here.'], + ['Keine Auswahl = Hoster-Modal bei jeder Datei.', 'No selection = show the host dialog for every file.'], + ['Meldungen nach einem abgeschlossenen Upload-Batch versenden.', 'Send notifications after a completed upload batch.'], + ['Webhook-Adresse', 'Webhook URL'], + ['Test senden', 'Send test'], + ['Nach Batch-Ende wird eine Zusammenfassung versendet. Discord wird automatisch erkannt, andere Ziele erhalten JSON.', 'A summary is sent after the batch ends. Discord is detected automatically; other destinations receive JSON.'], + ['Discord-Erwähnung', 'Discord mention'], + ['Optional · leer lassen, wenn die Nachricht ohne Ping gesendet werden soll', 'Optional · leave blank to send the message without a ping'], + ['Protokollierung verwalten, Log-Dateien öffnen und ein bereinigtes Support-Paket erstellen.', 'Manage logging, open log files, and create a sanitized support package.'], + ['Ordner wählen', 'Choose folder'], + ['Öffnen', 'Open'], + ['Log-Datei-Modus', 'Log file mode'], + ['Eine Datei', 'Single file'], + ['Pro Tag', 'Per day'], + ['Pro Session', 'Per session'], + ['Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session.', 'Per session = a new file on every application start; a new session begins after fully closing and reopening the application.'], + ['DEBUG-Einträge in debug.log schreiben (Performance ↓, Diagnostik ↑)', 'Write DEBUG entries to debug.log (performance ↓, diagnostics ↑)'], + ['Diagnose', 'Diagnostics'], + ['Log-Dateien', 'Log files'], + ['Wird geladen…', 'Loading…'], + ['Zeigen', 'Show'], + ['Im Explorer zeigen', 'Show in File Explorer'], + ['Support-Paket', 'Support package'], + ['Eine .txt mit Logs + sanitierter Config; Passwörter/API-Keys werden vor dem Speichern maskiert.', 'A .txt containing logs and sanitized configuration; passwords and API keys are masked before saving.'], + ['Upload-Funktionen über einen verbundenen Client steuern.', 'Control upload features through a connected client.'], + ['Server', 'Server'], + ['Eingaben erlauben', 'Allow input'], + ['Kopieren', 'Copy'], + ['Neu', 'New'], + ['Prüfe...', 'Checking...'], + ['Prüfe…', 'Checking…'], + ['Zeitlich kontrollierter Lesezugriff für Fehleranalyse und Support.', 'Time-limited read access for troubleshooting and support.'], + ['Diagnose-Zugriff (nur lesen)', 'Diagnostics access (read-only)'], + ['Zugriff auf Logs, Queue-Status und bereinigte Einstellungen. Passwörter, API-Keys und Tokens werden maskiert.', 'Access to logs, queue status, and sanitized settings. Passwords, API keys, and tokens are masked.'], + ['Der Verbindungs-Code ist ein Zugangsschlüssel — nur mit vertrauenswürdigen Stellen teilen; bei Verdacht „Neu" klicken. Standard-Bindung ist', 'The connection code is an access key. Share it only with trusted parties and click “New” if you suspect misuse. The default binding is'], + ['und damit nur über einen SSH- oder VPN-Tunnel erreichbar.', 'and is therefore reachable only through an SSH or VPN tunnel.'], + ['Im Netzwerk (0.0.0.0) — Allowlist nötig', 'On the network (0.0.0.0) — allowlist required'], + ['Adresse für den Code', 'Address for the code'], + ['Verbindungs-Code', 'Connection code'], + ['. Fernzugriff nur über einen Tunnel (z.B. Tailscale/SSH) — die sicherste Variante.', '. Remote access is available only through a tunnel such as Tailscale or SSH, which is the safest option.'], + ['Accounts und Einstellungen sichern oder auf ein anderes Gerät übernehmen.', 'Back up accounts and settings or transfer them to another device.'], + ['Der Upload-Verlauf wird nicht übertragen und bleibt auf diesem Gerät.', 'Upload history is not transferred and remains on this device.'], + ['Verschlüsseltes Online-Backup', 'Encrypted online backup'], + ['Die Verschlüsselung findet ausschließlich auf diesem Gerät statt. Der Server speichert nur verschlüsselte Daten.', 'Encryption takes place only on this device. The server stores encrypted data only.'], + ['Neuen Schlüssel erzeugen', 'Generate new key'], + ['Jeder Export erzeugt einen neuen Schlüssel. Ältere Schlüssel bleiben gültig.', 'Each export creates a new key. Older keys remain valid.'], + ['Dein neuer Schlüssel', 'Your new key'], + ['Vorhandenen Schlüssel importieren', 'Import existing key'], + ['Online importieren', 'Import online'], + ['Behandle den Schlüssel wie ein Passwort. Wer ihn besitzt, kann die verschlüsselten Einstellungen entschlüsseln.', 'Treat the key like a password. Anyone who has it can decrypt the encrypted settings.'], + ['Lokales Datei-Backup', 'Local file backup'], + ['Datei exportieren', 'Export file'], + ['Datei importieren', 'Import file'], + ['Alle Uploads', 'All uploads'], + ['Erfolgreich', 'Successful'], + ['Aktive Regel', 'Active rule'], + ['Alles behalten', 'Keep everything'], + ['Links wiederfinden, kopieren oder als Datei exportieren.', 'Find links, copy them, or export them as a file.'], + ['Upload-Verlauf', 'Upload history'], + ['Verlauf exportieren', 'Export history'], + ['Verlauf löschen', 'Delete history'], + ['Verlauf löschen?', 'Delete history?'], + ['Alle Verlaufseinträge werden dauerhaft gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden.', 'All history entries will be permanently deleted. This action cannot be undone.'], + ['Ausgewählte starten', 'Start selected'], + ['Upload-Ziele auswählen', 'Choose upload destinations'], + ['Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.', 'Files were added. Now choose the hosts for the upload.'], + ['Alle', 'All'], + ['Keine', 'None'], + ['Keine Hoster mit Zugangsdaten vorhanden. Bitte zuerst in den Accounts einen Login oder API-Key hinterlegen.', 'No hosts with credentials are available. Add a login or API key under Accounts first.'], + ['Keine Hoster mit Zugangsdaten für einen Check.', 'No hosts with credentials are available for a check.'], + ['Die Auswahl wird für neue Queue-Einträge verwendet.', 'The selection is used for new queue entries.'], + ['Nach Aktualisierungen suchen', 'Check for updates'], + ['Anwendungsmenüs', 'Application menus'], + ['Parallele Uploads erhöhen', 'Increase parallel uploads'], + ['Geschwindigkeitslimit aktivieren', 'Enable speed limit'], + ['Geschwindigkeitslimit in Megabyte pro Sekunde', 'Speed limit in megabytes per second'], + ['Geschwindigkeitslimit erhöhen', 'Increase speed limit'], + ['Geschwindigkeitslimit verringern', 'Decrease speed limit'], + ['Hilfe und Support', 'Help and support'], + ['Update-Fortschritt', 'Update progress'], + ['Alle Dateien anzeigen', 'Show all files'], + ['Aktive Uploads anzeigen', 'Show active uploads'], + ['Wartende Uploads anzeigen', 'Show queued uploads'], + ['Fertige Uploads anzeigen', 'Show completed uploads'], + ['Fehlgeschlagene Uploads anzeigen', 'Show failed uploads'], + ['Alle Uploads starten', 'Start all uploads'], + ['Ausgewählte Uploads starten', 'Start selected uploads'], + ['Ausgewählte Datei erneut hochladen', 'Upload selected file again'], + ['Ausgewählten Upload abbrechen', 'Cancel selected upload'], + ['Aktive Uploads beenden und stoppen', 'Stop active uploads'], + ['Alle Uploads abbrechen', 'Cancel all uploads'], + ['Nach oben', 'Move up'], + ['Nach unten', 'Move down'], + ['Alle Zeilen als Datei exportieren (Zeit, Hoster, Link, Dateiname)', 'Export all rows as a file (time, host, link, file name)'], + ['Alle Links aus diesem Panel entfernen', 'Remove all links from this panel'], + ['Alle Accounts anzeigen', 'Show all accounts'], + ['Bereite Accounts anzeigen', 'Show ready accounts'], + ['Fehlerhafte Accounts anzeigen', 'Show accounts with errors'], + ['Einstellungen durchsuchen', 'Search settings'], + ['Ordner wählen...', 'Choose folder...'], + ['Standardpfad verwenden', 'Use default path'], + ['Log-Ordner im Explorer öffnen', 'Open log folder in File Explorer'], + ['Sammelt alle Logs + sanitierte Config (Credentials maskiert) + App-Versionen in eine einzelne .txt-Datei zum Teilen.', 'Collects all logs, sanitized configuration with masked credentials, and application versions in a single shareable .txt file.'], + ['Neu generieren', 'Generate new'], + ['Neu generieren (macht alte Codes ungültig)', 'Generate new (invalidates old codes)'], + ['Nach dem Export erscheint hier der 75-stellige Schlüssel', 'The 75-character key appears here after export'], + ['Verlaufsübersicht', 'History overview'], + ['Verlaufsstatus', 'History status'], + ['Gesamten Verlauf anzeigen', 'Show all history'], + ['Erfolgreiche Uploads anzeigen', 'Show successful uploads'], + ['Fehlgeschlagene Uploads anzeigen', 'Show failed uploads'], + ['Gespeichert!', 'Saved!'], + ['Speichert…', 'Saving…'], + ['Changelog', 'Changelog'], + ['— bitte Diagnose-Paket exportieren oder Programm neu starten.', '— please export a diagnostics package or restart the application.'], + ['• Bereit', '• Ready'], + ['• Fehler', '• Failed'], + ['', ''], + ['Noch keine Uploads.', 'No uploads yet.'], + ['Pfade nicht verfügbar.', 'Paths unavailable.'], + ['Noch keine Uploads in dieser Session.', 'No uploads in this session yet.'], + ['Account bearbeiten', 'Edit account'], + ['Account nicht mehr in der Config — wurde extern gelöscht. Modal schließen und neu anlegen.', 'The account is no longer in the configuration because it was deleted externally. Close the dialog and add it again.'], + ['Account wurde erfolgreich geprüft und gespeichert.', 'Account verified and saved successfully.'], + ['Account wurde mit Warnung geprüft und gespeichert.', 'Account verified and saved with a warning.'], + ['Account-Check läuft bereits.', 'An account check is already running.'], + ['Account-Löschung konnte nicht persistiert werden — bitte erneut versuchen.', 'The account deletion could not be saved. Please try again.'], + ['Account-Übersicht', 'Account overview'], + ['Account wurde diese Session als fehlerhaft markiert. Klick = Wieder als aktiv markieren.', 'The account was marked as failed for this session. Click to reactivate it.'], + ['Pausiert (Session)', 'Paused (session)'], + ['Wieder aktivieren', 'Reactivate'], + ['Accounts mit Handlungsbedarf anzeigen', 'Show accounts requiring action'], + ['Alle Accounts und Einstellungen wurden übernommen.', 'All accounts and settings were applied.'], + ['Anwendung neu starten?', 'Restart application?'], + ['API-Key anzeigen', 'Show API key'], + ['Automatisch gespeichert', 'Saved automatically'], + ['Beenden', 'Quit'], + ['Bekanntes Größen-Limit', 'Known size limit'], + ['Backup nicht entschlüsselbar', 'Backup could not be decrypted'], + ['Bitte mindestens einen Hoster auswählen.', 'Select at least one host.'], + ['CSRF token nicht gefunden', 'CSRF token not found'], + ['Deaktiviert', 'Disabled'], + ['Der Schlüssel muss exakt 75 Zeichen lang sein.', 'The key must be exactly 75 characters long.'], + ['Der Schlüssel muss mit MHU2- beginnen und exakt 75 Zeichen lang sein.', 'The key must start with MHU2- and be exactly 75 characters long.'], + ['Diagnose-Paket wird erstellt…', 'Creating diagnostics package…'], + ['Die Anwendung konnte nach dem fehlgeschlagenen Speichern nicht entsperrt werden', 'The application could not be unlocked after saving failed'], + ['Die Anwendung wird gerade beendet', 'The application is shutting down'], + ['Download wird geprüft…', 'Verifying download…'], + ['Download wird vorbereitet…', 'Preparing download…'], + ['Ein Einstellungsimport läuft bereits', 'A settings import is already running'], + ['Einstellungen konnten vor dem Beenden nicht gespeichert werden', 'Settings could not be saved before quitting'], + ['Einstellungen werden gerade importiert', 'Settings are being imported'], + ['Einstellungen werden gespeichert…', 'Saving settings…'], + ['Einstellungen wurden durch einen Import ersetzt', 'Settings were replaced by an import'], + ['Ganz nach oben', 'Move to top'], + ['Ganz nach unten', 'Move to bottom'], + ['Hoster entfernen ▸', 'Remove host ▸'], + ['Kein gültiger Account', 'No valid account'], + ['Kein Update verfügbar', 'No update available'], + ['Kein Upload aktiv', 'No active upload'], + ['Kein Verlauf zum Exportieren vorhanden.', 'There is no history to export.'], + ['Keine Antwort vom Hoster erhalten', 'No response received from the host'], + ['Keine Einträge im Log gefunden', 'No log entries found'], + ['Keine Einträge zum Exportieren.', 'No entries to export.'], + ['Keine Jobs hinzugefuegt', 'No jobs added'], + ['Keine Log-Einträge für diesen Job (entweder noch nichts passiert oder aus vorherigem Batch und schon geräumt).', 'No log entries for this job. Nothing has happened yet, or entries from a previous batch were already cleared.'], + ['Keine passenden Jobs für Retry gefunden.', 'No matching jobs found for retry.'], + ['Keine startbaren Jobs ausgewählt (alle laufen schon oder sind fertig).', 'No startable jobs selected because all are already running or completed.'], + ['Keine transienten Fehler', 'No transient errors'], + ['Keine URL eingetragen.', 'No URL entered.'], + ['Keine Zugangsdaten', 'No credentials'], + ['Lade und entschlüssele Einstellungen…', 'Loading and decrypting settings…'], + ['Link kopieren', 'Copy link'], + ['Links kopieren', 'Copy links'], + ['Log anzeigen', 'Show log'], + ['Log importieren', 'Import log'], + ['Log importieren — bereits hochgeladene aus Queue entfernen', 'Import log — remove already uploaded jobs from queue'], + ['Log-Datei', 'Log file'], + ['MB/s · 0 = unbegrenzt', 'MB/s · 0 = unlimited'], + ['Log-Pfad automatisch auf funktionierenden Ordner gesetzt', 'Log path automatically changed to a writable folder'], + ['Neuer Schlüssel erstellt. Ältere Schlüssel bleiben gültig.', 'New key created. Older keys remain valid.'], + ['Nicht alle Einstellungen konnten gespeichert werden', 'Not all settings could be saved'], + ['Online-Schlüssel erstellt', 'Online key created'], + ['Online-Schlüssel kopiert', 'Online key copied'], + ['Online-Sicherung konnte nicht erstellt werden', 'Online backup could not be created'], + ['Online-Sicherung konnte nicht importiert werden', 'Online backup could not be imported'], + ['OTP wird geprüft…', 'Checking OTP…'], + ['OTP-Prüfung fehlgeschlagen', 'OTP check failed'], + ['Passwort anzeigen', 'Show password'], + ['Prüfe Zugangsdaten…', 'Checking credentials…'], + ['Prüfen…', 'Checking…'], + ['Prüfung fehlgeschlagen', 'Check failed'], + ['Prüfung oder Speichern fehlgeschlagen', 'Check or save failed'], + ['Speichere aktuelle Einstellungen…', 'Saving current settings…'], + ['Speichern vor dem Beenden hat zu lange gedauert', 'Saving before quitting took too long'], + ['Stoppt nach aktiven Uploads...', 'Stopping after active uploads...'], + ['Suche nach Aktualisierungen…', 'Checking for updates…'], + ['Suche nach Updates…', 'Checking for updates…'], + ['System wird heruntergefahren in', 'System will shut down in'], + ['Übernehmen', 'Apply'], + ['Übersprungen', 'Skipped'], + ['Ungespeicherte Änderungen', 'Unsaved changes'], + ['Update installiert. Die Anwendung startet neu…', 'Update installed. The application is restarting…'], + ['Update konnte nicht gestartet werden', 'Update could not be started'], + ['Update verfügbar', 'Update available'], + ['Update ist bereit. Einstellungen werden gespeichert…', 'The update is ready. Saving settings…'], + ['Update wird installiert. Die Anwendung startet neu…', 'Installing update. The application is restarting…'], + ['Updateprüfung fehlgeschlagen', 'Update check failed'], + ['Upload läuft...', 'Uploading...'], + ['Upload-Log', 'Upload log'], + ['Upload-Status', 'Upload status'], + ['Upload-Übersicht', 'Upload overview'], + ['Verlauf als CSV exportieren?\n\nOK = CSV\nAbbrechen = JSON', 'Export history as CSV?\n\nOK = CSV\nCancel = JSON'], + ['Verlauf wirklich löschen?', 'Are you sure you want to delete the history?'], + ['Verschlüssele und speichere Einstellungen…', 'Encrypting and saving settings…'], + ['Zeit', 'Time'], + ['Wenn das Backup mit der alten Passwort-Option (vor v3.0) erstellt wurde, hier eingeben.', 'If the backup was created with the legacy password option before v3.0, enter it here.'], + ['Verlauf', 'History'] + ]; + const deToEn = new Map(pairs); + const enToDe = new Map(pairs.map(([de, en]) => [en, de])); + + function normalizeLanguage(value) { + return value === 'de' ? 'de' : 'en'; + } + + function translateText(value, language) { + const text = String(value ?? ''); + const target = normalizeLanguage(language); + const leading = text.match(/^\s*/)?.[0] || ''; + const trailing = text.match(/\s*$/)?.[0] || ''; + const core = text.slice(leading.length, text.length - trailing.length); + const exact = target === 'en' ? deToEn.get(core) : enToDe.get(core); + if (exact) return `${leading}${exact}${trailing}`; + const patterns = target === 'en' + ? [ + [/^Update v(.+) verfügbar$/, 'Update v$1 available'], + [/^Aktives Ziel: (.+)$/, 'Active destination: $1'], + [/^(\d+) Fehler$/, '$1 errors'], + [/^Gesamt (\d+)$/, 'Total $1'], + [/^Verbindungen (\d+)$/, 'Connections $1'], + [/^Verbleibend (\d+)$/, 'Remaining $1'], + [/^(\d+) deaktiviert$/, '$1 disabled'], + [/^(.+) verbergen$/, 'Hide $1'], + [/^(.+) anzeigen$/, 'Show $1'], + [/^(.+) Account wieder aktiv — nächste Batch verwendet ihn$/, '$1 account reactivated — the next batch will use it'], + [/^(.+): Alle Accounts ausgeschöpft$/, '$1: all accounts exhausted'], + [/^(.+): Keine weiteren Fallback-Accounts verfügbar$/, '$1: no additional fallback accounts available'], + [/^(\d+) Ziele aktiv: (.+)$/, '$1 destinations active: $2'], + [/^(\d+) Job\(s\) zum erneuten Upload zurückgesetzt$/, '$1 job(s) reset for upload'], + [/^(\d+) bereits hochgeladene Jobs aus Queue entfernt \((\d+) Log-Einträge gelesen\)$/, '$1 already uploaded jobs removed from the queue ($2 log entries read)'], + [/^(\d+) Einträge exportiert$/, '$1 entries exported'], + [/^(\d+) Verlaufseinträge werden dauerhaft entfernt\.\n\nFortfahren\?$/, '$1 history entries will be permanently removed.\n\nContinue?'], + [/^Account "(.+)" wirklich löschen\?$/, 'Are you sure you want to delete account "$1"?'], + [/^Aktiv auf (.+) \((.+)\) — (\d+) Client\(s\) — Letzter Zugriff: (.+)$/, 'Active at $1 ($2) — $3 client(s) — Last access: $4'], + [/^Aktiv auf Port (\d+) — (\d+) Client\(s\) verbunden$/, 'Active on port $1 — $2 client(s) connected'], + [/^Alle Fehler erneut versuchen \((\d+)\)$/, 'Retry all failed uploads ($1)'], + [/^API: (.+)$/, 'API: $1'], + [/^Ausgewählte starten \((\d+)\)$/, 'Start selected ($1)'], + [/^Auto-Retry Runde (\d+)\/(\d+): (\d+) transiente Fehler werden in (.+) min neu versucht\.$/, 'Auto-retry round $1/$2: $3 transient errors will be retried in $4 min.'], + [/^Backup importiert\. Bitte prüfen: (.+)\.$/, 'Backup imported. Please review: $1.'], + [/^Code erstellt: (.+)$/, 'Code created: $1'], + [/^Diagnose-Paket gespeichert \((.+) KB\)$/, 'Diagnostics package saved ($1 KB)'], + [/^Einstellungen übernommen\. Bitte prüfen: (.+)\.$/, 'Settings applied. Please review: $1.'], + [/^Entfernen \((\d+)\)$/, 'Remove ($1)'], + [/^Fehler (\d+)$/, 'Failed $1'], + [/^Fehler: (.+)$/, 'Error: $1'], + [/^Fertig (\d+)$/, 'Completed $1'], + [/^Fertig:$/, 'Completed:'], + [/^Gespeichert: (.+) \((.+) KB\)$/, 'Saved: $1 ($2 KB)'], + [/^Import übernommen\. Warteschlange konnte nicht vollständig gespeichert werden: (.+)$/, 'Import applied. The queue could not be saved completely: $1'], + [/^Jobs konnten nicht hinzugefuegt werden: (.+)$/, 'Jobs could not be added: $1'], + [/^Links kopieren \((\d+)\)$/, 'Copy links ($1)'], + [/^Log-Pfad nicht beschreibbar — schreibe nach: (.+)$/, 'Log path is not writable — writing to: $1'], + [/^Login: (.+)$/, 'Login: $1'], + [/^Speichern fehlgeschlagen: (.+)$/, 'Saving failed: $1'], + [/^Test erfolgreich gesendet \(HTTP (\d+)\)\.$/, 'Test sent successfully (HTTP $1).'], + [/^Übersprungen: (.+)$/, 'Skipped: $1'], + [/^Update fehlgeschlagen: (.+)$/, 'Update failed: $1'], + [/^Update wird heruntergeladen… (\d+)%$/, 'Downloading update… $1%'], + [/^Verlauf exportiert \((\d+) Zeilen\)$/, 'History exported ($1 rows)'], + [/^Verlauf gekürzt: (\d+) entfernt$/, 'History trimmed: $1 removed'], + [/^Zeige neueste (.+) von (.+) Einträgen\. Der vollständige Verlauf bleibt gespeichert und ist über „Verlauf exportieren“ verfügbar\.$/, 'Showing the newest $1 of $2 entries. The complete history remains saved and is available through “Export history”.'], + [/^Wirklich alle (\d+) Links aus diesem Panel entfernen\?$/, 'Remove all $1 links from this panel?'], + [/^Zugangsdaten für (.+) bearbeiten und prüfen\.$/, 'Edit and verify credentials for $1.'], + [/^Läuft (\d+)$/, 'Running $1'], + [/^Fehler (\d+)$/, 'Failed $1'], + [/^Update v(.+) verfügbar\. Klicken zum Installieren\.$/, 'Update v$1 available. Click to install.'] + ] + : [ + [/^Update v(.+) available$/, 'Update v$1 verfügbar'], + [/^Active destination: (.+)$/, 'Aktives Ziel: $1'], + [/^Running (\d+)$/, 'Läuft $1'], + [/^Failed (\d+)$/, 'Fehler $1'], + [/^Update v(.+) available\. Click to install\.$/, 'Update v$1 verfügbar. Klicken zum Installieren.'] + ]; + for (const [pattern, replacement] of patterns) { + if (pattern.test(core)) return `${leading}${core.replace(pattern, replacement)}${trailing}`; + } + return text; + } + + function createDomLocalizer(documentRef) { + const textSources = new WeakMap(); + const textRendered = new WeakMap(); + const attributeSources = new WeakMap(); + const attributes = ['aria-label', 'placeholder', 'title', 'data-tooltip']; + let language = 'en'; + let observer = null; + + function renderTextNode(node) { + const current = node.nodeValue || ''; + if (!textSources.has(node) || current !== textRendered.get(node)) textSources.set(node, current); + const source = textSources.get(node); + const rendered = language === 'de' ? source : translateText(source, language); + textRendered.set(node, rendered); + if (current !== rendered) node.nodeValue = rendered; + } + + function renderAttributes(element) { + let sources = attributeSources.get(element); + if (!sources) { + sources = new Map(); + attributeSources.set(element, sources); + } + for (const name of attributes) { + if (!element.hasAttribute(name)) continue; + const current = element.getAttribute(name) || ''; + const known = sources.get(name); + if (!known || current !== known.rendered) sources.set(name, { source: current, rendered: current }); + const entry = sources.get(name); + const rendered = language === 'de' ? entry.source : translateText(entry.source, language); + entry.rendered = rendered; + if (current !== rendered) element.setAttribute(name, rendered); + } + } + + function renderNode(node) { + if (!node) return; + if (node.nodeType === 3) { + renderTextNode(node); + return; + } + if (node.nodeType !== 1 && node.nodeType !== 9 && node.nodeType !== 11) return; + if (node.nodeType === 1) renderAttributes(node); + const walker = documentRef.createTreeWalker(node, 5); + let current = walker.nextNode(); + while (current) { + if (current.nodeType === 3) renderTextNode(current); + else renderAttributes(current); + current = walker.nextNode(); + } + } + + function setLanguage(value) { + language = normalizeLanguage(value); + documentRef.documentElement.lang = language; + renderNode(documentRef.body || documentRef.documentElement); + return language; + } + + function start(value = 'en') { + setLanguage(value); + if (!observer && documentRef.defaultView?.MutationObserver) { + observer = new documentRef.defaultView.MutationObserver(records => { + for (const record of records) { + if (record.type === 'characterData') renderTextNode(record.target); + else if (record.type === 'attributes') renderAttributes(record.target); + else record.addedNodes.forEach(renderNode); + } + }); + observer.observe(documentRef.documentElement, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: attributes }); + } + return language; + } + + return { start, setLanguage, translate: renderNode, getLanguage: () => language }; + } + + return { normalizeLanguage, translateText, createDomLocalizer }; +}); diff --git a/renderer/index.html b/renderer/index.html index 86424bf..c673fda 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -146,14 +146,17 @@

Eine neue Version ist verfügbar

- +
- - + +
@@ -165,6 +168,7 @@

Uploads