From 1f9e093d7f3ecf6be7b14cd55ebe770ff76b47ae Mon Sep 17 00:00:00 2001 From: Scott Small Date: Sun, 10 Dec 2023 10:55:39 -0800 Subject: [PATCH] Macstodon 1.1.1 - fixes a crashing bug when loading lists --- CHANGELOG.md | 4 ++++ ImageHandler.py | 0 Macstodon.rsrc.sit.hqx | 2 +- MacstodonConstants.py | 2 +- MacstodonHelpers.py | 2 +- third_party/PixMapWrapper.py | 0 third_party/__init__.py | 0 7 files changed, 7 insertions(+), 3 deletions(-) mode change 100755 => 100644 ImageHandler.py mode change 100755 => 100644 third_party/PixMapWrapper.py mode change 100755 => 100644 third_party/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f2f1ce3..a3d9324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## CHANGELOG +### v1.1.1 (2023-12-10) + +* Fixed a dumb bug that would cause Macstodon to crash after authentication if you were trying to sign in to an account that had no lists. + ### v1.1 (2023-11-12) * The timeline window now allows you to select which timeline is displayed in each column, using the drop-down menu next to the refresh button. diff --git a/ImageHandler.py b/ImageHandler.py old mode 100755 new mode 100644 diff --git a/Macstodon.rsrc.sit.hqx b/Macstodon.rsrc.sit.hqx index 8b34dcf..abb3529 100644 --- a/Macstodon.rsrc.sit.hqx +++ b/Macstodon.rsrc.sit.hqx @@ -1 +1 @@ -(This file must be converted with BinHex 4.0) :%NeKBh0dEf4[ELjbFh*M,R0TG!"6593e8dP8)3#3"$NM!*!%eG*6G(9QCNPd)#K M+6%j16FY-6Nj1#""E'&NC'PZ)&0jFh4PEA-X)%PZBbiX)'KdG(!k,bphN!-ZB@a KC'4TER0jFbjMEfd[8h4eCQC*G#m0#KS!"4!!!$NM!*!$FJ!"!*!$FS!$$D@P8Q9 cCA*fC@5PT3#PN!3"!!!q!!$J31dqi@3QT!#3$3i#Y3#3$NeKBh0dEf4[ELjbFh* M!!&Zkh*cFQ058d9%!3$rN!3!N!U!!*!*Thi!!$K"Q"B!!!d!$J$V0E'Car+X-lH ZfiaYPKC0IV%VcqZLXTFlf6VBl+2mXJ%EQ,hm2(qKhfmlYQ-HGd[Vmr)@%pb3!2I X1[2+SpYS1ll`$RVQG6aG,2i*1GcXZS8[h0[Q'H9qRlE)XRHp"Vi(G2AeAGG"[[! QGIX)*&!#66)!*I!*Y#1r4)jYL%rJNB2`+H965ZeS9@SlhYN&qFLZVQqS@e&IYhT j#l-,DjXE9kpHYV5j*3rCNMP,DeYD'jFh0P3dYc6Aq[c+VjKBX2#5DTpbhF'Xkk, PmDFlmV-2kX`p(BfIE!NJ8Gq`SV'elT1Y,EP)6PhEfYVBd*+&922D"PN"iQ@kUXK 8eDfZUffY(bJ`(E*4X+bqBENNQYDfV'ST4Q*0AF2Da5XE@aZj@jQTC9ApLYE9GFM m&Ajm#[%,qGdLj6V)cM4[@mR(T`Umr+eY($"9bbI9dipHrS'ecrMp49)RU!!ZI&[ `)-jdXE2SZ!h[EAMIKRF(hRq!JXmZd6(cV'-pqa"R*dhBimC0rrmHdmqpphDUXkU AU2a3Qr*20cX-TaY`Mar#ch32qG+VZYA-H')kL31U5*@S8PAD9[SJhXJ9SHErM'1 fdEha`DUf84K'Bi6Mc-+26i%U5)f$T"d(UEILS$T9M94RU&(iRB(-UDMi2m1Bh6b iXDd+`j!!!'8crNHAP#KeDI8Pdi84m%cTGir-rEfj[`I1m-qY8@TmiHJ'&DLX8#0 @PPcpLFXM+aGFGF@#b&K5-,jclAEAXLPZ@c"aI@&@KL[!Z'9qKJRJ2(9@K6NA ePI0H981@M2P&[@q2e!FiLArF&EH-AMch%q(&U"NkGmpRebbYEm!qZ)1jRjLlpXB jeh"0XaVU)6P3j6i`mk94P4qX8%1`Ri+c'P6Ce!EhX3d+dD-c'pa%CfYi`IJa+Y$ C-[@HcReM4ih2kIc8q-,16k2fF1GYREFMY`1jTcSRMUP`RfS+GYm9VR#IEJiHf(C 1a2h4Q4AZrYEJS6[2QH)qXHHcHpT4me"ipVDcCcHSS5Z9qXljH8Z'K"H%9kRjiIP &PDUi8UdX@M!0+"QkCmCpdaS19@EG1Z2LL2[B4DKiCXD@D3ZR0rZHQ(Ea&2I4eZ` RVeEq+[q%8jkXV[*hrU'HBRb`+UqU`RfXmlVCda[2DXVH`akUmT3peAXDh%HU9,T 2KIYS9@lRGGMG!c-rT6CdVQ#QFfY9ZI*A9VL2l)RX@EJJ`SkSb"fSq!0XF1cX#K9 B1@*m!G!iY#S2@#`CA`Jd"S$@U86Mq!TJT`9SZ3E[(`YD[RcTFd@pD3a@0bpGVp5 3!1#jaaqLm[$i3S-q)$)m2iA!dBZ"Z%pKhS)8"UXZ8ZGeET'Dl@EZ3XaGD1FQ(ma GHiZHHQEpbP@Vm3Em,5Gm6PG1@phB3NfQ9,@[E866k*QMC`qC-f6@U[cEI0Il)rk `2j`Gc!i%X[(cYCeqY+UhURq81mTYT"4a0MT96P9L9++d0`mrP3m"J$Ni6((EN!# Q)CF1Q6fQB)arj[%jG2fSSk-5SpJI8raL$PqErS@Z2rHQMFkj0i@ZYa@F)HY"r6Y phq4R0MUZZp'Cr-cTqf`9'Z6KTp6Vl'8d9a#rrkbjG&@Qif"@#V*89KXQlFikL[H $b!d-`ATrQk`qL(I%Ilaq800p'3pLD[$-3ke45VKJR890L%FLRQ&LjP((HUe*-Xp *#lDZRkVH88G,Zr1`FhSCEce9[F&J)*b0RIrlq[qdpI46kVS2CP9MiipMia2-aRr 9le(CqPYE5fhG*lq6%Q$plqYPEQrVrlA[53ZfqBmBrRAphfhc2rE0qG%55'P+$Yp H+*!!rTcTFpG#mca%*3S&'TJ%PAX"&"88DE#S@[P@"kBZE2)Y8ElVV5Cb%f9Me26 ,jSd[Q,Ub+UHcSh,84@Vfp*ACIelG%$MrZLD&AKpV8X%X&9&K+,EV"cZC,TcSr+, TkIR'A$`G@fM3lkU,`L9IIb@9$3DrrXV&eHiHC1AYeCZXe&rL2SfX[,ekNf@pb4j #0Y@Ef@a[8QCc*AX)lb0Hhb0i[qle48D9kBc1MN'Amb[FBjI0Qp+[eXfFhhP6jDJ 2UYR6'V,AU5@ccIkcZ2mp-bj51PqC*AT@DLB4&8AIZD3Km*e,9SaB-'e*fCpA0rS A6M-6",jE[ITq9(hcr%e02UYM2pF+p`q1C(0p`dVPprd3'(CQ[M5bNNl(qF!`X&a b`9L9HpY9bJmG1c3m(pSl-!,)V4bVr)X`Hq@3!$d0DJK90VES9'CIMTUU,2SBbD1 c&A4ddFV`q%*XVX*e1Url%r4Sc+fkb2eKjbDJ1iI)TZXKQld1@lQ!@lQ*ZRS4(-e TGDYAJff`P@q5@E!9XNVC@(JQa"Tij"+VV$mbVh0IH2%j-pdMd-p(2)IMfe6A92S VP2m-ih3mh6NalA#m2KS1ai%GibVF!`IZVP$l0qh[!'2PVP'q"Njh$V`'pdR`eC2 EcMi,d9N,RMbLKPkm`(hLBQ$#[4p4YV[[BcGq[2UFfHih,mUZ+)$[%T[Gi$kamXU 2c-2@RlMhM2$LdB(CQeE1$Nq6HA,GAN3&EJ+lk+9E8A9+CbGc`$1drkh)F2,caUM J&b+9[ZkC4fEZQA+!1*FZil2Z(38mRe&dRXU[9#Z'AT5eB&TipTiI9!3UXfqG"Um PYL4h`E3rU9kcIrEd0I!i18Y6F-mUjBH$F[m()N!b+LV(5(iImVQ5KdGb(hE6@-$ X"j!!J@H#hGahYFTGA6Bq"jXm"[FU&qMk*V#p$mJ1(Tp$j[TPj`Sh0Mi2R@qhRA8 R3GEB*@I-VSBABhYGT,,!JBr1R)pq$HahcN*JjQSJ,C'CH@%%fqjI`-KKA@!-b#+ d-K#SJ+,T@eN#Y$b+16V!T5Z@&*RXMF`belPekXV1,848jhEKL))9ZH)*RCP#c1" @M8Y6+"l03r"Sb$6A5%5IjYXrpiI'9&99UH#+&C@9QEF+$ZCAV"!hKq`l![`'VKY +"@9B@2Z18aG'9KEJ(FElr#86i&eP9@@,NeJi+QIUqqCq0r%82F-PG,A5P4"-2Y4 qPl@DBdpiLhMSd%#rjMfSCHM'a-D%cPMeTkY,ZdZlXpTF4rqbfTJh(E4$8efU+Kq -K+H(1i*hh(V(V4h"kH&)Z2*Sk38`EN[3`9qUUKlFf,fafcfUIma9THZ*Y03FNA# Ec0%@C1lrcU&rrca(F0%P9bbDFpEbZDUcmY`,cKA-&-'5%@KSGFcSB2SjRNE"B2V J`F*#cbe(5KjQj-hNm1($P8UpqIb(r,jpqj4+[IQFb'FHGGh!JbdF6kX(q@58[a4 NLNlNfp6emUL!MYY-h"kLCq%,K8*"(hkKd*Q`%INcmCNfIRpP&Rr[pf,p9&kQ(j8 MiAb9#mD8C`8HQ23XN!"BU8K9JMlNNP(GT5qZY$r`9@*Ml`C9e6fU@pG[e,D(IG, eBXCFGPR%IqqpE3@AAEBNCrZ0-mGqT1kZi,9`2CAk!JbEF&"XXHbc!Z&`F0(3S39 MaS`jDp(3XmD'a`5(AKSF1UCeh,Mca`bpB1c(,KJkFX,3F@-rqV1RRPacj9pmTqE 9UjGFr246fbTfrNAEkGMA-&bKP1b"%ECQRr)rr@hPDebPe00l9"EFXMcAlBCVGR5 8#kASZJQmZEArr8lJhB[h8Fc4AI@UfeD&6A2ET8G"hfi#-bH8cqf&I%+"1GXi3@) HYpRRlI*dJhI+[3Dr+$F0IP@Z'i"2I[@BmMFGVpr@f`ETqZ2aEqYYi@rVhm@(qEI MGd9r8rrErLF$U0r,KrQhBb41NP$rL`GYE[le`c&q8rlV"SB*8[QhUc2e2hmH-mm [k[qTJIImYX&[*[M0L)19BMKV"5HUMSp4G+,Z"XZP3ETF'Q65E$$)d8&)3MZX9SG T3[EbjShF3"l[0r03NNUPhP5@rj+(!P8UpDBLr@rjc'2dUhhQ(mpMkB2P1MmrPGE j0%46ZYFm&-5C2"[)NfQ3!-QE"UNK"X[952%pJ-rT#h!IakGYeV5jlf(Lqhr'1QE bTXbIIkQ'm9A$&Ph69"GZA"'ZEDjEfPUh2,bLIR9Gbk+j8qDF1Sd9MFer9hcDP,@ YM@Z@YYEA,PfpqTT`5e0p3lKfEA0,Bh2ee)mX#XeYE!f[EB&9(DjGYEbqHFcC8kI 1@P3kT4BY-%HiTA9TUkiTVUjE[I5Dm$3FY$DZVQ0&B%&G5ehcZVVP6"E1@pq3!#N H8GhBm,l@F$dZkZUAVUlr9&hi`f[VDkqQHmIbd,bQZJDpT($,UUA0HSML"8[V@qV ##fI0Q$pVrL@X++ZTDfl"9@ei$1jl-8rYUU80+q[1jQ),jkjGXh$YLKAeYA8YmkE 0AC3cPXqP#aG0bj@b6l**UB"Q"E$4iY8-XA!C,22kA`+dM4h5e&bhSUkjVU'f,Va 1li"i1A9@3fYGme*FqUkV#bpGJ9biTEDj[UQ9a59BeM*FLiAVec3e0NY*B&j6Drd DV*r*i'8i'Pl"UCB$@-[V'eP89&fhE1h+F"1ZQHX%NL-q9&IA&&k2Jq('p@&X[U' aHFh5eH'k6pE,4+FG,kpVEXBDEIA)ZBh!A'T&M4JI6CB$JidVfD!-$CBfVk`GMf" GZ'l0fY@J"VhEiI0@Fbp,'jB[E9k1L@VVT(Q,EGY5heTh6Y-eVDX`8-[D*VYLN!" 6`IaZZI#-#&8K+q6eG[C8GEPDjl[4[m,kHm$DNdT9mSKdL-kl,lNpkSpmI3rkI@r JeVYSSElGHX6[rrN0-5[jh1PlAC9G-(A@R&6GM1BEQPEpdE+V[2YLpVVY@Qh#f,j m8[-%d%XZm,aRpJ-`!RR2DLpk'%(6q4PrqFXdP9`Ac*l0Q',%A*RRXJXb-*a%!`B BSemqBr3VB)aqK6)0R%p1Jhl&M&&4`KMpB&CpqF[S&f5-IL'C'RMKe1Kh#Q2d'mS BPF-BSemCBr3E,YX"pVJGp$Z0-IU0B)aqTc0'Kj'-dHpGXN@PcZ!@mBC44M@V`Sc 4lpf-dHp-aZMd(R06IaDhMGal'D2ID-ESpcl'k$H'-IUG,DK3DLa4J8bjdFMM'+( I1Bc4Vi)aqSdh#VU5eHKh,Q08R-FBrE",`FF%aZJh8DC3kRa1JAiIB)aq&c"'jB@ -d@m5Br5E,0-UG4'R4Eq,'D2I"aQMAa9MG)J`4VmTXK9m&S"E3EpTM2'ZCSaqPc" '[qQ-d@Q'dImcZ6hdQm8B0E-CSpq('+2ITBc4EijX'5Fkh$)bmaLMhhc'k2GKaZL hJ$(k,43d+,A)A!YHaKJ90Bc4lh*'k(F&Br6lL&3TG5@Vd1qMM0([Bia4qA('k2F *aZLh@,SVYB6Gd@mTBr4EaKMpDKQM`h,'k&FR8`)#R",p9M*'[e@-mDjRM(jA-8D RUf8Eq-J$Yi&qDaLMA`0Me$Bb4VmQaZMhHl)eI'5"@d1QK6(kY6*'2dJX`FFkaZL hAVDVe#HjAI5lKM%U2X8Br6l0'2df-%Drc`J+P2TpSJ$p0M*'[cE'U'`h5[jD4ZL hbHMkcc+,IYFa4Vm1aZLhQ6%kA-mBr@k3!+j+hFLZk2FjaZKh%f2dZjNahTpRM%k h'0IJ9Nk(IRr!'2fq`"MprT!!-ATmN6(krAqb"D@qa#dJmkmBSppYM0&2d)YqAf' -IPq9E5QeKGY#[kf-8A%lBr5lJc(kE@1-IRI+9[&j$Qi9rAB`4Vql'+2bDic4lfl 'k(H2E&qT6QiIrEl1'2hZCBaqhf#-$Ph'c0NTN9,IC)4qIm3BrHjMM(jrc"MplMH 'd,HNQe,IeJT0r@['k2FRM0([6aQMhhFBSaIF&F((GcN9-Yr6AThkFmESpaH-dHm "aZMhICNH9aQF([ef-8E&Ac*'[am`4Mpmkd,`m@pN5dSpa#fKhm1-dHm4aUMmSA' 6F,dUq2LhXNd-b@fLhlpMM(irBSaqZaQM!qiC""Q2DaY-25&Uf99rVGe'"4dYNrq 0GT[88ic4DHq4)i+2IFmq+r'2[iH0ihRkhRXPrSQa)*pCMm-U2-q+BkE8rQUj9&I 2dE6$dfe-a1H9NSN1S"8R`Xf!6(5)Ap0!r!,Y5F3[+L86[84l$I&2F6M%L9l@0U! kV1e&p3U(`N4r+m-rUhl''"2*eM$4UpS296bIi%5[DF01Ah"M)VJ%-Y%al@kUk"& j91aCH962pq44[II+SrU-DGUrAKi90bjKSPSHP6a6(Z@BJi6-&f1XCk$MSRCArd6 `Rk`a'rkHkcrX((DFl*H6MbGIGKaQpf8k4(lS9MU2GpecplDl0fc$Epd'[%lm@,K Y`pehhRhh2FQG,cZ($fFQQ2dMGj+cmfjTrC80ReQhiFjeL,qbBGZkE6EkM-3bbfG d+0'fZhFqIZc9c#cc(RHVR([39SqJqfcM8&rGJ0Hkck!!535FjdicKXbMjpT`6r+ ec%#,rYUYhSR9b%['B$rdiNCN5X`LXl*)$m0kVQR$ZM[e!YGYfhRi@'DUbrr'RC' 8)@3`24hA)j[6"E*jJB6H%e$"lD(X6[C!QK0ZfaR0$2E4[Hk((&Q-lLXE%JJJ")T BJC!!G9m&V,J`@DQ!j5[BVf`5@2fU,0,Tb3chLAhZh(Y3cA9`8hKK(JdLVXUXE"X '*26-e"b%`fZ)F8#-+98EIYVGPjR[fUIG$c[TRM+U`49fMN'j-XaRpL[Bdd2HbEc '-9TU1-XDlMR@Mr&qjKE(frY6[q5pEUiTb$89eqXGY2c%AHKS('X3!bCkZb3K'9" 3ST%[Q*40F3r%&KH$(6'bp%1qkBl,&P429+R%[BMM+Uj8h!bpk4PhNB11-TZGPle "@SCTG-4aZ&pZ9YCVYLbFB#L-&!Uf&DU`8bH9LX,BjT[6pbD9,dVP+39Q#CpkeVh Fd9K1S3KM%33D@`5Jm)@K$#cI3-)M8F%#@8pDJLA3FX-pKj-$fmrUNFR6!1MBlei "j(1C&Q`b[ic*DBAb,12U'Yd+ba"')5PBaK28##kB[eX2EiEQfarP&R`pdB5A0RZ iCVmlcj!!29XE,YGVjFi00H[C-N$P,i9G!aQ0*'G$VKB(McYrc`'IHmlp1-K9")S !3%K6m+eCAp1J'3r9KRMZe#XbV#RU5(1aB9a$e)GG-lbMmM'rI[f5#cCdZjp)'P& JPBi3S@Bh3`BD$jjqXA*+j*X`Jd($9d40J!M-aL#bU)Cq#BAV-Ef@2X)#9JpCc(% fUd*dTF'-*3b459Sjk&Bk-)Y$pZ8d,IE,"R+j#E`-(pK0E(cHcGiC#NdXG%*C%`Y $%j-Ka93K5Ta3(LT#K40$KFP3,M05G-Vi%#UN6N+q*XC$"6V2!%d8"[%Y6Y&58ZA )q#NS@#"HHm!YkCD"*KD%#S0)M(G#C42e02`&aiG`QBfSB',3#4@-Ca(D",Q,d@J lS6!i-B4ABE!`9-&@@)+d#'%H,+-J&()'S0JEar'mE-1#i,S$lXLGD&dKD`J96JJ &#c%@)d`@j%EmK41"JU(B$XT3L29`*C`BeHq@9@&N@6m@`LD-G6Y%Qr3+b-GF2[' S15V0#PM$qaGc"8%Z&CX[#2LLrS#''0*k(aJBp5`K&,#*`N6S2@aUPi@YB1G%%RI #jYa!#,"@%be$%T&@,)P-5#(5GmC"plhG')4S%$`84!8Y2TN2b06l`(U6q8-$j`6 bj5frFFMPj`ILq6Q"I"5Giq3(aV'H,C,jEM"E-+i1[q)1FC)$HV8pRR"HF)ZF,PX 9lh)1ZB919lYNQ8kkElK'bjEhU%"8p8NZrUSl**NB(+BpN6cNPLB6!dAP+GbMCGG J-3Gme4dj8$2`3rdc'UB2((6p1`QR2)!%#bd!``Ep&U*J+p"4U+"Aj8Ba+RjpJ[) X6Tj3Tl!NrI)(!@@eme8hZkZQ*aCp9IPlqT1(AApA65`'RUj*((DcR,iH*,1M[A% A%&$4'2ULGqrVVYrTlcQLr19pE*RX+rmCP3iC"D@Bi&9-hG[Z[1kqJ&&kir[erZi lk"BY"Sj"$3,`J"D*TIPB*dN&6-,##5b$*aap4[N$K8(ITZlAA9pA$eaQ020&qrr @9Ie(PDr21F)e4*(U5A)lCYAN&acYplb"TM%5cM REHjrc-pF@Mm+NaLG9Pk0- A2DamjI%hA$MF'-2AQqc@M24HEJcS)Xq6'dAP*P8`QJmD)Le+1#&8B)N0NXG)ApQ &@3Q!B5XJ#@-&[XAGQ$U10D8%)eUM3EPce28P)965K9KkH4I+fQ2pK,BPlkL[hBQ k8$I-kckFX$IUCMYF!dM(0)YKjZ44P!%@@%$4!!pSH4JP`[VH,!A)SX$"N!#"XY4 3LHFeX(FIG)X&``EBT*iJS%ZZeSUK-*3IlG%)`$8!m8RF[Kr@RHUb$JSKVh'%G3( bF8(5ki5mC)q#L6LlAr8k-F0'l!HDkiN40H3l6YD$PPeSQBJ4*VK'-0")S%YF)-S GS$@`JKZ'&$49c3'p8"HiTk5#m##Y&ZE&94jDj&2XJAl*#0"B"G`2MKLXrb2liJ* XMR5&Ud5LbbRA0-`eqhUFBm#E4aKBC"`&5BmpHNP1IGba)4!0A0@,NPilC&`9H'J Xad$C53+#iNk2,N#5Y[%H1bhlfQTJScr'QdbTi"Y)k6qSLFphb#ef3S@DUUN[mR[ 9+3@8PL)mYFUeE'[*liKHPTEG)[G9M%MYXrM)X#4@d5kii0D5k9e'Ni#T,Y1L&HZ )*RTFElBqfJJp2Dk+feN)I26V!jZ!1[@-l"d6GBP12Cb'P3"I00RM-Q@hRY4EF`a G4VX1D3Kmq*!!UhBkS@+K-1!p9)($9h6,TLi'#`4%,i-(JMd3"-SA,$!r[2+pG@1 X2&hN",-3j[F4fc9BTej5GJm%APFbUEU`c5l$41#0'!-ZpkRA'iiirAJ-"i1Z &FDck[+@T[N3bVb[4hf[l1VfZNmc#4#,jNf+j1%Nr#RT"-E%qNQKAXUZ2*BDS`*B aepp[mHR[6hBPX)aNdPS&HSTH9l9VA'1%hU4H@&,e*kh3!#C"1!,2U"i!-bB`3lQ 'P(+KMAF'35TD+p"Gm5@K,J*38C6em"H%@XB(0I+,m6N$X2"%S!a[5++5!*,)S4Q #!-#2PFPLS&A+Zejc9E*2Ub#X)J'1m8AMI@#6AJ2(GQ$!L9%Ip2Hk@3Q`!NRrG@d 9JC%5Vi0Jf3ZX!Z9$EN'fA2J6,8KBaV2J@%R@3%-31U5cAMGY"NHM'Z*a`U(r"D- R$lPP0p$#JD#%X+#2-M%r%-J[)+'SB$+8Vbf68)(PTT)mX9,S+#(-0m4+`2P%Lq5 L(k4)[S!5)-c#Z`H!k"0B(i@'&hld4F%,eU`Q3!!DRp-(e%5G2PFP0"'`X,m(#Mk UFV6dK+i@@@@%FP*)&&-NX9dY%E&G-!Zp6X-6!!PCf("#!Y0K6#eI80R['F2BNLK AB&C6-9EaSSE*Z`qjHBZjEk`p'4T"X8@K668*b-$j)&&3IJFeL8)mq'L*!!S6R&" f)6iF+N!KI%J@D#J-6X,Jm[TSJSQZHY9-6jf!65@0RS4LbRE+$@HJ$[3LJ)4Jm[8 V*1!9D$F,c3!-eHk4,@K$La,GfZGi545#@p$#m!G8QbFr!)dXSf4L[D4+6d-5(aR E6%1%9&S#,dA)%Ea*`ddN#6N9`1J$#XLB-A)SF94"&9F41LF'Ci(-)D)FRQZqRSY +PBZ1BH5q9c9V'+(Bcrec0j3Kf(mA#hX)Db1rDLLqS@UiaclL3YXGp2k4&6V&@X% BTPmIHP[TCXK#kp4%[`[iD*k`%`1V[9VV%!L)D`a(R%+1i'E"%8lSG-+IeUP3")a LBJ*T@VKLcZLh'hAK45C##M6Kdb+YAa@,U*J!P9F"UcII$)keBdQ`jP65J,qQcrA A'(d$55pBLHND$A'Yc9K@mdC+8U3T9,M"k%iLU"rle*T#R!)YX`##,N0*3TL@N[T %UeXUX,4T6*m8FCj+9X#[M-B,$F(3C2SBC5'ULiR$86*m!Z$4+iSl6k4Y%)DfId+ SY0mBCAQSLBHbdBb%Q8fQV$N#9c-10c,e-ZK4j9LNQ",D4mV#GrYNepKM$C%$cGT $KG#IU&(p")-"')$KMiQL"bb-5FN3N!"506TPD02FPaK@*%$LD8Bd!LbZ"4$)aHc ##M$c,MI%#A0h@0hNiC1'6bkE2&aL#I"c*TG-QM`*5Ij3&*6@F@J)9Z0AKPD65m' T`r,BG$+lSBKU3ej'5bEKQ[k881BQKIiXG`1dX*N&'Z+*'0U)GI9klVKH2%NMTKI (0,K6GU["e@p9&Zi*L$RLSBFeRVj#&FJ+T!SMbI*Qc&#%rj!!1hTa'GD*pA20CAV C5*FKTaHYUmXdmjcL'bk)`+S4iAjC3c8lQSH+a2"XYRprRb`IL'k2#[DXZD+PJ## "9a$a&i&YVA0m-6"DjM+"+cHYK5G6VK`@VkN%T5)BE%YZ2X88)KZd@-$fdkT"Ld3 #dQ"Jq#(hIH!!)3!$IPQ`K6NAVGNK#,5&1%FHLJJ6YTTFU[Qm1+#K)c#D$2T8Q"C MeSLEDYHIpTLdB-Lb&U'qKhR4pIGjMK`k3BMe#c!%di"&&XdV!6eiJ*K'4L#4dP" qb&RB-1E)S0r0iLJBdmT+)qF!#i-UUb`0&eYbl"Gb)!DikSP!JL"$f%(c*4FH3QD #AQd*5)#Y0'Q@@N9&DM"B'Pj@ULP3qjG`C3JH[R%'RZB&Re2$)X2@II(-A4Qf*I4 JYL@HG-V)*"3dfSq6SN'Id+,(!F#!jJ84"m#)*B9KKpcm1QjHJ1m-(iRPK@6K%aP Jrm#$-!@qhBM'`4JfMJDk#89!R$kJ#Na'PH!(RB885)f`N!#59)D@%58@+HQT"2f 'Br8#V4P"+#9P8XJJCF*SBB#jK!fd4(Q,)VA4B'mFL!B2!F#$Vjpq+D$!"PBN$8T +3`Qq()m5*TGKdq!k`&r!3-KVJ'Kp--Q`SS"INbMdJJB!&*-QNmR"M',5G%NV5Kc Dech+&f+`S[+J*M',(&"N6p)aLXQbQ3!M)l-j$d&Kj,0(NY*0k-%i$IfH9`8mH$E 1#GP8!,r&B`FYRCcK3fAl(N9+XN`m+4aca*!!GSD,GJa"0HV*5kLD"!A5&N+#Y%L r,ZPGU55lHQ$J'R)bFN(I!F3231(JYLE@jAThC8BI'*V%kM@0k+Xh38#r54Tfp&` 'm+0C2'#3!!-*EH`FE6335pCl-KV@US3q8%+GL"l$L!*YLhQY%BC2)#!Qp22ZL*E #j#!%Xfi!Q3"E#YT*0+@`![5M6%R9+UiU4hG-N48*Z$E"q8E'BZ8U%pkjS4CC3J3 LRB3IFD`eS*6B5TmU#5QD6BYdP+CD0eZU)3#Nc0*!pL(hl-8LDl4'-QbJIpcS",h rb@9P!Q#r6j-!G3*i6k18YceD0k%f#ee4TpeE+JAk3VU2T5($Jb)8M**%ShJha45 ZD+,Y4LSCYdQ1RJa-hY3)4L9jRTX3J4%TKJfY9)`Cmb!eXN9"#3MJ"Nm23aH)$UB Z3+3"))3!RM0'ITiR)e&V0%&aJ)V#SNK6)EF[rQ%#@PTF5-emj%'Z@'4`r$PlfQ@ 20N38p0'X5lY,iX4am5"&3b8#J8&[5B[a&#PQC*,)$L-c-jD#PBk@%'('MYCq!TG -6D59SCB'HZN%a-3bD#3jTY*fJV'KY$JJ5R42kZi3YB'fhQM,@JJ6M`DK3JGQVlL `mhI&SUr*EG44)jeNTi)$E5'P$(Y2'%Rf68Y&+`*S4)d&Ae5%SRI*aKeBDMD%-15 3!([Q$GJ`P6,))1-DF+0!1@[*GY61QYf#QJ*C#Gq*NV"2P8%H@F"3&Q5F06LYf*` 3J+J$E6AiSJ05m9RS"l(9[6-$RJq9im-6'E[H8dc8!N+4eUp+8D3FHQ3Xad'MLGB Y9Q,B*N269MJVNU4@#BBQ$50SGD`90H0*Nh$BBXd%fA`TpN3V3IK!H`q6bc*m)-j K%K6T4j!!mYL%c(MX%Aq'NPTILP&Ca(!10qJKD#j,QI)r0a$HdNR'6dT*CZZbTGc ,0#ZFFB0RVfQA6-0"E!1kMFl`E%*!0$2[+LQ-*TTQH*GkfU-i!'QKkkJJH,KLpB+ REmJ+DA)d1N,&Im),),emkBTcXN&r(PV#qJLCm`h2C00E-2VCqRD%K8D4U%Gl$1& 4TCR*q!K@+N4K24T@5'Y(S8K2$a-PY"jMT!9Y+iLF%Yd-6Q$$#HJ@*$kdHXN!d@c #mj8d+1-rPNX&cCTk46L42AkXmBj5X*V4+ZFhc6@VR+eL6&'PTB)bN!$K6R#EeAY Pp!1d4d$P5)dJ4KX3RK,hXQ[+!GCUmG`[$SV(!m++9TkCQBhcBH5Mk!9K!bX-pm& Xd8ejKA(mMNH%PYN!U)",MSZ$N!#f3Bk,JcI009iMD2eP[C68dFB*j8M$p3DMSb' $a#F``P"E+YCDQS`V`e,iJGlC!PU#"FXd$F0c0#U"3*&GL,A@5hS%')bAKEXR+J5 MPICDL@H10A'50AMZ*6)*$BdGRp)*J[I8BBDP"'ZSD6i@f3L6`EJj)PYPbV5P0RV rT-Ll*N8Q48C1363b%LRMDmUN+@@4bIL0R"bC0'ANT*'6Y,3UbBZ8S3pVbb+6KT% Imdak#TT-SSkQD(PE&aMdHiEc8lLRjRfJ,&EfEVpBBHm3C&FL$Nr)!AY-5$f3!$* -2CeNd#kU)-91U8B$*'f`8(6)cIP@*$)p-MdCm50b)QA6)j%C8L%r'd45jNjaRK4 0dF'&(MhD6SLRAjKD3'U2F2XJ'ceG)d*"+aPHU,6(bjAcXZZ24`fF0%d+eV8CBj@ #mH'e*$qK(`F-*NmR(Ep'X),"E-P+jp-1ZIj[%36KIE*L4&-3Q,e+`Q3[M)%9BaS &'L,$p,F1Z0pbfic4X)a85JXFNS2K4KlA''kdd$EALefi"daSa&!%p9VKC)aRF4` --)hMjJNJ6U!&P,NjY*SD&l,@9l1'!SkAY+L`VYCE9`S`)0rMD%LNYbkKaBSXh)R Sr46R6GG0T2a#66[&H6-XVP#D0Pp9Z@CDDfF)!FJ+l3&JkZj0V%)F"'+C4Z`3&IC S"HHGXPlVaH0+6,#I19j+hEYjMP2U)K,AL*Rcd-'MK$-1ZB(R,"diN5aQR%L1hVi 6+6CiB,R(@RP1T'"kj%cGG4KN53kTNZd#QMK5"S'f)DhL-8V5+#9ZATpEFIFm$%8 c2b$$hVcCar%HKK$e)1GZD@i!,Y)N!kD`)KScTHR!m+Hp9-J#2fRF#%pB(I[Q,8r DH6N9dUSl63el2AV)%!Cr94V``CK18da!5hMVcSA'Q"+C6SbK2p5%FH+-a,8HZ5& 3,XS3U0blk+YDR-$+-6*Z)h(JES4h&k'42[h5N!#bPi$kD"TR%IMmb!RU*$46r'V dYMeNac&cq[$VVGX&A[N8G`Xh%"-Dra"F@($TQDc#PNFDa5"RrG4BQLDK6SI"*Z$ (9I*qE0SL8NCBH0jF8KpS@B[5#%CFpHV,0dTV(,adP3[liE6FL'KI15qd`D8b5(R k*Kehed53!#"1e)@p`GCR)jBE$DPDbj)%$I#)#Q@-Q6eEiUflKRIKaSCi%$+NB!! kR%MZp*&!2c0&&JA$$1iS$B#0*(S)D9aSMAl$,-)faZMRKBr@ee3@Y#`eZF#lPA[ me&'1hfNAj`"FJGX''LSak!!jKT%$X[D-MBQFf$+D$%!G4ZrMpYr[a-`(*Z`()I5 (!*,f!-LlV"A6"XI`eJCpjq!Kqk#VJ))cZ0@p%C!$QP3KX925(#3#*cG25be0Yp3 CH9&$)L*8TJrccZ%)#hiH)AU-&PX-9+"*Rd`IVe&J#1ePp@-TX+cmXCkD4*p@f@3 qH0TL4qL'Z+lPe`hiRB*MFT&X[RKL[kq#(Mh4'Z0mNk(a!33LL5C%Y-EkJ2b'`br 2(*b$lY#k33d"E5"lKq)5mY5e)LSFXA1K`6ei4-5Md1[22eGNk8LKAQ@%"9$IDdd 5I$c@idjXXBCjl+J((cU4bhcpY4KMm%GKm'HeS`)hKGLJTNApQ4mYEBq*5[LT9QD #$'%H,TeYa"2ZmHlZFFhqpTA$k)0ZcP9QiHrb[!9CX!F*JIk&8,bP",eK!G!MJ5E fYZ4PprXL3`h6D+m1fpGILG&,mkjIK5MPQbM-kLXBI"q%hcFLN!"5(di"4D!@A`U 5%heVpjL2aPM"*1mhq!dDDhVCBbG26QDq'U2K!1Gam1XrQ+dp0L$38jF2+SE[H&f Pd@&4iVN0c%**D8BBQH)$#%dMhV2*NBDRmd'G4RL`N!$h)!PKLKJqGd-@`#H-Bc! 1NXDA*`D`a&l3J5mH`rH'j4XiI8I%`,!(,2KB8,C$'@l1$I85F&1)6`EaNaMkNd' 8Q[JU5L)@IC'I#l&hG5Rpp0Dp3q&"0eKRGQZh[pH*K+a%S1NL`V0+[&bK!X-48bk -UZ*a'Hdd6&-LPZ[)Yc8bhp#ahlJK0lhKCY-k-KFK@%HdriMCEkS9N!#+)c%`b$& Ya1&V3h'3!*pR-r%VAELH`KH6j1YKpUXcH*Z23cP'DD6H[&[(9i[H2*Ld3VX8*2$ jY!rM3F)S"8mh6I,N9JQ0200M,k46h(ai5S#LZiq-A#LR(eTYmb0DjIb@8mUViUI #M)R&`jIbVPHi9HXmp5CIJCd#HC6qE"%r&*D83`P0"QcENrKE0q$)0h8-jFVYJj% 1D*L3!0Zbe#H%r0%q%R,+lAlV"-*hd"ef``Ni'$94+NZRe$D@&@")jdA8%8@M++F qFI--VeJh6eZDVlR&AHhiMTY(d&`,GK($&f!50HA!ZK[ePpFNIZVQ1-aMC6JhMF9 kfj1Zqc+%CfmjA6TSbUaNIimX9m-M1aVVkH[kUIkUJIl1NT%(Vi0PiKK&Ji#IaiU 9ijY[Vk9[SNi)b,k$lSMlh`5!i3*5I"k9035fGKaXR69T`+8#&1mPKS9IV!V+FrK rNHRmE"hqi-eK0kMrPP(kPG5PVp#C`AXrX-"#H%B[kNV6i+I![Zhj%fRQ4ck*6)) Yp3bQYkipM0UNC$MB-GXPJD6qmSpU21JZhHpa'pGRR%LV#5cq8rBFAPSa8#@RLic `TS!F!0#c!jpqaX"CY!#G#'5&lNrU'-Q48VV$Sf0Q4C9U+)ffdNfA@6@6iI&pbr' 4[26!mk'1Zf[VDf[V%l@U2PRVVkpGhSY-$1qqfQ`8,UphDY996Qh1FQR6,fd8Lq3 GVeAlq4eAEk)e"pb2Eej@hl'XpUCkK[8G#&CeV%,-h$+NGH+QCFbYfVa++Y")GeZ &+Uq*edahC[L0lS&aVclJ6VLjSk1@8fk@9dIpjSjD5D%'K6UaE20Q"XZNB0RQ9CZ GMN+@SjG81adPf!%U1m`Fp8k([ljMFq,iX"p0V+UpQAeN24L'hI8,!jU%ANBpTPb eHGR0b+hLe&cd+URP@ZXh!`'eL2!#@,#,cGd$`eje`,hdIYN'QR)e0q-R+q9%k+N 6UjLdbqH)-MNEF&!ZC9QY6)'&&Yc%KX6$C[N!XcGVr3(h)dPC(rGaNd%!Keh'l#S %NLC@Z%9*DK`YklJ*Hq&f1260'V0SJUQP9he#[MMYMEVbJ*Yc6&$UG-`PhJ8A-N" (,@D9M@RXbN[MP5K+c9q2QBN!rHDQT"*IC%N2@[HmHpPQF!')43!M2aNF5q0#08i jS*!!K*R-rM4UZ&$d)[5)Y*ZaX-hG!am"VhhHABMjL"RGQ(JK"fP-D-!6l%!V4ZF -8U[*LY9BRF"CpUkKK5%(2Q1qp(Rh`edHk-aSq'&@3DC3J)bQ%F&59!PI##F*P!A CJPQ1F$qmT25)LjphjcQk0aS)TSP0TXQ5k+ha)88)f%&M4T!!*!!NC'TC[UcMCXc A2I$Pq)mrlmlT*THarUCD'F`Z$DR0E[HhZUlAh'9`54!4QSL"B8S%[3(-G[pcMYX pm+AlqGhZk4TVdPGcT1`(#+QpkA$FIEH6[2rQcCY[eMc05ENk!"S$IIiEphGKB5p Q*VS)%pf2hS)hYK6N#KpaCa$0MT2eR2-'qQDlar"rT*l0G"q2lJiD#83eEJfCb)+ 6X,H12C&T12BjGi6Q%%-P'YQD&$YZIZ"%VrX0@)@QY,MK@X!cQ`GkREhI2Be6i`@ %#ZmC8ZIk"mIDlij)lRKi+elm2E,MBIaZ4fV($Z5qmIe-Vr,plZR((YQkDqZZ(E[ 3#c&rAL,ji%#cC`%'&+0m1lTY3@EAMLfl(XD,&FkZJ@E2Z+I[hV*Vki-F$2'ZV9Y hlGU"1I@`cPpQQShlL6Yb0kC"$aC[hl9G,f-,FeXIhR2XVc,G2[!6p`cRNDhF!"V *SVESMJqcD1[$Z`qr$"pJcqlGMq#6`1R'98qli@1BNV0aHGZjGDD`3GR3`l[h(([ G,6bfCmrZZh6YRS%"C[rB2F[C`E9Jj+f-X!1ZIbZQ*JUfB(f2l0lp0@HhZXYjT%3 kS)Vi`"S(CTQeeahh#'EK)[6Qf!5,`!Ee0P'XYbZBfHjXc@FR!',A8pd[C%DCmC3 EfQ1fLK'f!fK)19[2"$iYR(GSi(*FENehfl8&hq3D(13$api3",*DTZGqYa#BK!Q c@!SA+K8%rhCChZ"b,hR5R3L)#P+!N!!Gfi&30#'bGh%I'"%,i4BHf[S3B5pJ3jI "I8al`RhrEZk5)h*HEP'3!-Sek`d!*Hc12@"FU4RFaC6(B5VYjYU!pq5Zpf)K3Kd #$+!*8fY3%0!5icHiKkVGl[KZ,N)MM$JL+)8iZ&C03DMJ`[PkH-rKJjN"*MrQPU- rPb"$)b88bYl#BPbV)Ee(GRFI2T!!DAlqSqjS%*)"VCk2T%"UeIL@$6bmfqPqEN# m21b1!JZ+#T!!cTSKQ'$h,BmmmMT-YSabq)%lJV4j&hL*6!S5H!LbiDlGHjaMhHl KTc1pc[J"T%rF,6MfaP21'miEajaMcZ1CqZ"hAAAiKbH$r+mIH+P+mdp8l"q*-rm 8#irp0d[fhhr+re&LfrccmXq6#[l(cAIq3j2ppjYRB*S#rS(8MHj'9ph@TU[Hq@q GhMpP'[A5lB[`[J[DPrp"jRI&Khr+C$d$L32mc`1iDe5&q!rQq0Er#hld+P!iqf4 +Z5F$5*8`j3"3MU4,QIDpm!,Z-&p!N9[-[(S"jj6-jLSA8,!KPS2$9TqEfT*i',i [qEldckk*r[p`llJQfHJPhXfSPqlS([A5YV18QJ5Hj,p8YUk*3DAIS$,,U2jXJmi FJmjFSeVc$%S$"UAj4RAK6hB+@JX0@SZ-DLJfU#daU-ANlk!@Ip%d$L$NUP+Flal 09dGaZ)TrQ&bDR`H#bI-bkfdQcmZXYjNm,i11b(NCVkE*b`a1PX8-!&GUaMI,%)k j(lrrKFarq+r)319R4lfdr@1`ZU&mI[jIMJH3!&+9`&pjEQp[hp5qZIf@pM[Dfr[ DAfYr[RdA5R`42ljM3"GZA2kiNafiF&Xfi)dC,elm1qq$#qFjAM$3m*Q03[JVEU( #Cq4mKC)6Amfa+AaNbUE`c6@GmQ2)Z+h#Ab8S0(CF(!hJDZK8XK!IlY!TTa$ffiN C%4CLr,F012T[(Ek1NahiEeXQi!d@QNB%R`6$ImY94a(#$T03[$9kEmc!@)1P*U8 `eN`Bm"h05klh06'KN!#`(ThRZ*(XdSkE0pq!hAD5",mYXD!DErc4f@TbkcrkECK bbi82I1ElbHmRlqLljEA0cfq#hpD@mY[1c0Fki13&IP[2Rek%p`9+65rqhIR"EaX m2#ZKLqEXaGp$fN[lV"J0%R[4MJ&--AbLGb!KTPZ2Y")VVQq[FGV`H88-B@blj0k 8dlCh,jffAjfjB4lIVEjE6h,JXL8q'-%YeAdSr2T*09`f16pVi[NC-hPHCVeN2'X Y6ae0')Y0&mMa@j1ajG$ACT4N!ZSS[[%iQ-"rEd$LT!YqfiIaRJRmcS*'q5Hrl9T BXTrhhG(hrEilqMjrBFH&eelBKPpNN[(EcXmrrb3(IPYLjUQM%R2`crSr"+Af1rD $hbE'Q(S#hjfQ-BC[F$mK1A(4(*d5Eqd*HQ[jk"Z66,S)RjGJ+ll0jb9N#TZAVdm S,ih"HYp+mGXAIC)liF&pdII&NajiF0r"qaQP6L0@6fEK`HAldF'"MhBdh`pfBDE dAc,kma6@JeX23mkkFDSTGI"f-SHlYcP&Sa+A&LNeiZ*rp1'Qp@rU'rMpfDE6VXh Rj2EZ$4EGjrr,e[i(Pf6Yr*ES!UA'3!HP%Bhr@#NBpa[-CaN1b$DFN!"M1#,AF!B 1%B9$!SC6mJh(&"M1+63F9'3iUGK`9)RK,,K`aYFEa%J%R+pGlk-!XX$DjFG,5Kh mVkAVEc'C@paHQlR&CRUCb5Y&%kR1+d8S(E0+EqQp"9pNejPHQh&Y"NfB-D0kJmX DK%IZ`qqr)r,@!4E4R2$[@54VjlHKP2QF2IBI@!6rUd+Z*kpTZklYDfeeYephjc9 hqllQZp0hHjT&9$Gqrh0RqVZ%lqc-M`VCDGE12pCA"qTpbjP1lFb``BP['Ekj3p$ %YV"[8D!Y4rP6ljaI[br$cKIj`e#(l(eb"&aqDi2C-Cf!Im$PP!1EI2)lGq#(EmP iZ,c!&fcMG1G@R(X1hZ2#Acq[mVcccN(`r[$#fXE@e[$#08YAV`i[E9JHVQeXD'f ZAlDfYE'jT3*06dhe,TkcY,DPYA&jBd-B@Eh'(Dd`NI$1,XMUqGKMCr,kHQ4@XaS kIpDd4DT-hAYTp5A6m3F8(TKffB+&+YIh&$krhk#br'0RA2DRX`$Z+qFh0U'lIm@ kZZB@r%2%j[VD&Pc(qQp%2!(aPa#r"r(f@G2Q-[jQIHeUeRmA-HXIQMUh'MI!rLI QR,9m,Z,pdaGJAZ8r['M1r%Xa6fcKV#X[8IkXNZT,jmh!&XqVRV8)rE)qZ,bfG4R LfEiR&E58N50rSdj&#)m(ZDF8r!epjq,EUq$9q*k6h$ie&I-4'FVhBc8(Z8p,lJh ULRIT@GUBqkTi(3UIi94UUhCRVQ@ZDElN0M&hMEj@rbacR`E0i,P1FZ,lU!lQVS1 ma,1CZ3l0J2M6bLM!R[(F`0bYUb9h)h0Id-[mR16JRq'jLERE2bLjQjQl!ci8RXm cYqfcNVZ&Z6YAb5l"2@(PecMBUjC#C1-21NMGj@'9)eqi!PBqKYaqM4AfcKfYqc& AUR[[FpeX&45fm[eBFPG)lQR*EG-c5Hm'240cH@4h-e2JbY4-q@0%3BV[&i32K3I rl48jZ#KXiVTKYAeN9Xp(Eq09"h*A)2I&lrmCHi3%PcSh42CKFZ+KQ0bq91i8q"i f0eaV'X(dDI$(j&N$rDe9Lp50b0Hl8@$[%82eEK6q&-VTec+P0HI)ql#26Zi*Z3H 3!)11N!$FBmMG"@b"#dBqEA"f%h)@F`"dddMQ#KHYElbd[U'1R&TBZlUaT@ljdZE Qa[AjM8ee$C)*TUYD'PIA,bra5L9G)+RQqT@V@[-PYEaaI82f62"qp[6kPYE#U@a 8em`"5qDYEkKV$MIAY65ZEDkYbbCKCj1YFkBXDe`l%2dr!!!RS3!!: \ No newline at end of file +(This file must be converted with BinHex 4.0) :%NeKBh0dEf4[ELjbFh*M,R0TG!"6593e8dP8)3#3"$Qc!*!%meK6G(9QCNPd)#K M+6%j16FY-6Nj1#""E'&NC'PZ)&0jFh4PEA-X)%PZBbiX)'KdG(!k,bphN!-ZB@a KC'4TER0jFbjMEfd[8h4eCQC*G#m0#KS!"4!!!$Qc!*!$FJ!"!*!$FP!"$D@P8Q9 cCA*fC@5PT3#PN!3"!!!q!!$J30mZiCXmJ!#3$3lX5!#3$NeKBh0dEf4[ELjbFh* M!!'r"R*cFQ058d9%!3$rN!3!N!U!!*!*TiB!!$M4!*!%$`"#`G8+ql&3'UN0#U` 6bShAUJX*[KTYHi9#MEkZePPP9p6cF#P)MH%$35*,hd1G`5&V1'YXK,P9iX`r#T* KFXAA1,(k"hj"!-NDZc5!#XHNl%*[ffV!*1Df@eejmP5Ck9Qe+ZJcQ$P+$m#jiZA @U*chD&!h+SYL'BI4`aAUR-))aD"Yhkb#E+5"c%pf,BG&"bQYdq+6R@K4h2KB`2h b`YL4#A),,[UjJ@R'HDHCh)@5X0*JR6#2aG4rTZ*EXS@!r5(L6kF-HP9eQY@'',B Y%N#!dFYB@RBkD-,[H2B4[Cl#HS@VTQpTVP&TU&6'U2"pSRpBcj!!!Gm-pq'R'E0 @"1KJ[ED8AX%%&TrCY&,Ca4V[8N[!RkI(+a&8`i1&Hf`ADE&'ARpJ'ME2S%*B8$R ,kY%2Sc5*pZUE[)"hkAPapFf)PUNdFL18+l!cj,SUX`Dakh'8d@kDYAJ4'"PH9%* 2cj4C5BPd,'FCLLE5AV!aVURf@4fq1GicIE(Ff"50ZC1DPSPL!BBmLSBhIcTkFr! p%@!1FM#-+6cI0m@@691`*r$cVH9'IPHNmIUe0'`q3j8Bp-NJF8T!UZ8U+*lj!)M Q(1+)FU(3)%j*[k$cpbhGTjKcBK%U'8`3!iI5AfpU[!PN,1cT`80YlHl(1rfie,A 0PKHND8XCV+iURZ#paD43A89jq%a+8IUVA5kfZ5X@%fei4r-frDf##EcUpDKU&%F *L@&`aVbl"TR+q+&H9IULjeC8eP*RdfJ-[V$$JNL3!ik1!iJV00h2aFpT[)6IVB( ,@*%RNMC*LX(##@bIk(N%8E!TRBaT@(a"REr4U['q*Tr"JD6d@0CYpT6#D)c5S8T kPZPH5KBdL3'elrVjm[EhSRBhP4I3TbQ+IHAVIX#"hX2YbpG(4XT'Ef+)YZ*4`S! DRp9@2T3cP[G'PTKDj%D'Ga1lYUr#+E1r0TfJL$0-piDqP29[LC,1mcTFZb+4FCj jc$`9YF*a@Yh3'j'Qd!!1!e1#H()c*Dd&)*3cdGA''ETd"'Qk6h9E&MI5fhh##bj DIJRBJ`E#!D"dLqU*3r[jjhapQqB5(R&"X[q!T"5D+bVAJD8'P@GDkF@6mKm&ckq $TTi[i43c#pGp(NT5TYh`R(3`63Lk"4"$Qi!S*Lba3*jd%N&bfS'&i5!)!M4`Z@a MFXeTpJMQPpIF1jI`U0,GqKM+!ILUlRM#U1@'N!!F0j`'JHQq6rqrMV!%@4#TRpr +-CV&H4Md2'3HDMT6$p'Fd80$e486%"+DBXmIK!c4(`)q1pM4*+1c'8)r9Ih+RpU K5HT4qA(qd`4!CiK&p)`rN!"L'&%3m4$SXV-BA2X&0La08rZ!ejKb@eTi1U6%0cj Y&!0*CkJ8#iU5fRaMZPh)r&'$rKV$Z@L)*Ffk!'0Md"QYpI9@b(mGh8Mr&1D3!+8 +MN5TKTSTC35Vf`115![dlim[pFhai58!P,(41US29Yf,P[l2U!XJ%e&#h)D'+VA 9mFB-`#8YV+YVDb6N*caXhXQ8'Sb6rSVHl+)S2%VLc)bC8%cL9Y5e"kjm,$iS$hU )k-)m0`0GLhL+'$qT(e2NL@cpF+!rq$%6CIDQ54ZQSPmPZk4Id6%4(e'bP'NTL-j k4DSdBX+B`)(8FXND#a)EZSB4ZQkU,&A!CU+akJ'qZieC9mK+8``+Kl`Y#cU[k%6 4bq)C8Y8L'dZUDlFji+16rlh)S($STK$5I`b9SPU3!+Ma*282dU84UGA"`dJJ2im e,rClY@XN4[NJ@3G,T-0Ah+YNCjLVU&FhaZbKF4ka2!p(,8k'&hPmc&2VEG(rFR% Vkf81Qc)ZGlY3hdCKSF'1dD,JLNJA51-YYM0RKk&fl3"3-DNpKkerl342iA+mi[6 #Pi"21`FS9**lIbQLJ2H#EYdaHQ3%0%%3V&#U9AbQa#fl1B(G5!1Yf5Fd1a`-(*& CDKMc5LZXjZhYH5l9IL$D)P[F)$['A(8PL*LJL`E)CM11)jV!akHlXmb--4Y8HM` 0`h#LKbML$ZY#(2Pc-aX%GC8LfCG!4)"M,l'QSfm`p&MXVL5!SCINqKjl85!CIda BPmHV`h3FXTl&%8GXc2")k%`Qah1XjIm2IC+'P[ANID85@%+FKqXKme8(0-!TR!c *9kCkPcp'JUAr)&6bi*F4%[&-GAfE'F!L9X"iiDcUB$5!@TCU1FGjbQ'B+-@!#8T )%p2TY(JmN!"Y21J3@0CPD-P)D9YH$iDA)UI(U,rlaT[CLdC8QB)*ZPiJ!19DhLI N-BD,5p([##`"cQGc3MY-U%Ch,0rpJ%1lA&LYkG!45XhZGFEDP2rDikGp!9h00,# EXdhBhTL)PHJ@D6jNie+GQ`L$Y8jcrX,[QYC`phA30HGkKUk8#61dr+Ld2Lr1ZG` L3a[QkDZ%Y[Xr'qXp3pc35dFB3KXfqQVC02-M*-KGZZV&5AGA4C)VjmHI#9h4FV" %U%2qq'01P3X+Ll0F(PYM(U*T(@cL@PB%*MI+m&&VYYAFQP30(aZQ#hBr(Cq`Sb2 IaM*Y*[I*34F([DD*Fbi2piQjJURjl@!!E08NSI0T0-Q(C)T$%L#Q#lJN`Idi%,U (cAIrKAh*2XiK950+E83e4aUB84Cl#5Xeqip4jlT+i2jPXSfB)GXRKX1@h@3jQhq [XDJmNKJ$4QK8Q%*Y1NrDZ%b%&E3%P2V"[4QXlS#HDNCVFAV&iq09pUQ@jkclqF6 `A"ZN8B2&5N3+eQTEY[2G@IQf0CpKQNSQ'H1Ne9&)5@""L),%qeF5IK@cDJkI[Y& QP,a5b%#@H2qBH5GQa55&3Q2dUGlJQ5Q)JlTbNIUi-D`V(YrVhS+fZ'#+Ra$%rSd "9JrUcHZ83p4Q3mJ2!Y(`T34lfKhkm4G6"QQrdf8lqS!21P3a+AdrGN)XN!#!kS3 'Kii$FBKCmc$VmNha'aDKG6G3bf15Z2%aL!HQGRfT,8J-%[S&*Dp+DDGRq(jB@NP Qfl-r8LH4aY90Vh2bl63HhMFJH@Z6e0!LR)H+j[98,dlYHVBQKKi%jTJENJc[dG' 52Y%qAT(Hd6NA)Nk'NPXP*'$)"&D#e@(V@)-aA(T3aRam!12q3i2JB[25Nde(ae8 MqdJQ#4GlaUcd9mamf)Q3!1Rj,@6$dTj-"9r4ETJ@B)UKK(3ilICjE(kk&%5I"%d mfY"E"A*60"%P6'E,&d4SF,VZK2b3!'F9CBbcJ$QS[8XqU836(r-#1(N9aiN$L%( I15qQpfmFr9FCA`Di3')3U!1URV0V)%ii#fGSD0AN!kmNr9cf-U1+`1,*6@fAP6m !Ni8Mp@c-NjHmL2!,I,Vf"TGq(59CATRTl815HIA-+3dBeJkF6HNa0'F0INDEP"I F4%FE)V@*)NkM)N(0k%N4ZL#H)%Sr&QNa%mTI!eBYcKpPq5pUm4UemULpPQ#IP6& Z&0BJXMJb+KCm!*FMcFYCRkbS@a-$RAURd")HX9jm61aH1VU@i(G8fAZEe6bN)YG c0-%2,@MHYqlX3FrHT[#C@p'ZjmBSVXYGC23QQ#%4`QmQYeTM%[A6YDT[p#kYSeb 8CN"M0DB%*"!c9Tcbr30!2)6X5&4)96F5#KHGDDG%m3@K#JYVUK%V+XZYMMb*(Ni JQmT8lcdcJhHQVVL6Yr+)5h$'#plF%!r'``%NFdm`h,Tp*H#,R54&IpjbbKpQqF2 C!lCDqPme#5-H)r(8U-#k62P(6qqHkHk)Br9PY*2I'X2Ym)HP0B3paF#XXKYN"5c R,TQGTI-pF1LTq3mJZ0QL!JJ[Iil'[+@rUP5cDU,(EZ8*jZ)'IS2lH5QA1m&8Ml$ )E$[`q$T'q)G*8V49BbjEcSe,"IrKVFr8QpaMXRL3!+9[S'1!cFJH9jbF&q+mL)J TpeMr[@SE9pFMbqrdQdZ0$T1R6(riB+C05&UJP8I16H[m5,K#HC@"c"6cqR(4LVK 6E1%23FCGrfDVN!#5&V-P!&d!$5CG+Ne'pZ8ii[L'p'ShqA1`bEJ,&p+4@km9Xdh k@U5'hEYa!`2p@%hDF$94K)8kiqE8fBT$d!+rF2+2RBM+PQABJ,$NNQMF%6erq`5 %q+&8#U[`G@Uq8cP'keDY5NlL53Y[XrL#BL1f*Qh!,4kFQ@(ea(5E5D,)*p+Q9mh 385KpQEIARSUC5a#-%)3b&$E'@Gc2-UGcB9(EMC&!ZGF[Hr-D2S[`E@3RDIEr@TE $U1HR56#PDm5bD#SY6fG-aGeGeQ9SpM(AVH"(F2lLR'bEa('SmQh($d46lSZ(4!p iEC*m$%YFSEBJIiPR8&TjQh`Z1eYXSP5"baJ*mU(Tb@QBjI')khSZYe2@aY!C"1Y kk[FqkFKLEDQJPf!-Z,2X108mKX1[%M%jSeS!afUXE'#3!2)"q6F@S0DX43Qi#4Y *%B6(Ec!56(iK@AqJL'LicQ6LXdA@U0%(fSd'B9p,@jMJbZJ&ar+MrcX'"FqP!e0 X50lJH*8'%bJQ'X''f`R"i3b5JZQSmPBK%HJVJK2*HM3NCm[ab)KH'(2LE`(0Hh9 Y(S[9F&qV3eFe5B2FL2Cc!jNKfD0(*r'p8!H2Y35k1`Q[6$r*I"mIcUSNDj4pF,I A1m%+(q5lkU`-26PET9%44Ra-ML$lL#1,2)#q'kSZ6$PmKF6#U@df@EL$%6M8i25 iM%1L2"Tbm,d2(p6Ckp9qS[NY5UUFJ5XSGFdK4'I-%hE0i4&dFMf&6RB("%Kp*3+ cBdq$9qd&$S"[H"5bP!kh0Db5A#DpjmE#)V9$N!$F"4X4RpCX#mFX&G!rLP05(EZ K"CJZ6IMlRB8I0jEMNYaapNMFkAF!!A,1@6p9666&`YH#J[5d,`j$RlX@ML+`XP# !ir4Y'3"5X)HQrYfPT"pRZHG`BE4FElE%KpTA-Qkl42U02id5#JYGVqM-D+Ab"iF mBlH-,Y"*[p'*6"Z%Z(YT4jEXfrAE*4QM`3I+r,FI(DmfhjY*5aD%a%rS6Jp''Q, 6eh9M@kXFTd,1)M3+dQ%)TFUCS6jVk#A2h+R"CM5('TBSR#[cY#EYBb0iQCaiBhZ kI9mbX*c8Ycmh,MJ6dh9@9hlYD14[-6&Rd3pGL!#SHJi3I4JjC9J9I&2#NV5Cl[r 0pm)6G3Y9akI!Y,%iYq-`RqkC)HT-C43R5fRjL!lf6Z3rcqfUX+6eb8rG,fVbZC[ ki5QhSI!8heef&3p8@"Y'),1*H3Ef([+iQ#PP(M581A0ZjM(KeZA$&`C,"&UM*k) 9DqFCC!02L`BL8$1'AGp91P%-J,CmCHi1iK(YY-SA2%!+lCMbGqB+$NA0FIF(SRS j'd$614@1dqhQ60j`$LPbq86h$,+8#LZkl#S6phN*58'B9De9Q8#,$$e"l#*TBF" 1%RQPZ4+b)EBSG`Ce6Kqm,$[R5l!%X0i!q2IrMY8A1*0kia,Pm#Y8Q3+,&C!!MQ) jEU!P$H3a(hqUFZG06ADbSP[+l2YDL-jr8)KH$c`cf2LbG*PVTX3J"YV4$qrLhTq bP1eLE&%EJI(6DEpk41ld%A$9KI`8L2qI$!fG[H!p4#8S5DNH+Z$46rZe8Yhj`m" 3PbU'Gdi6"ZSIFJ!a%-r)p9)-CH('Vd#LZkD)(DQbEYVE8I1`K5MUAkl!'IUTkh) P[@EcMT)FlGpK8V'!95)IA0d#(iaX"a`qC+eVpAd3`*!!eYBc6*QeUeX'&Kh%PJV 4LRU['H,*B4PK8-khXQB-24VC+hc!(T4lELHfVP&LBekSqLjr@!bEjF`YiTU,p8' %6&r`C`bp!4V'(!rVf`X5lp1!+Xj9i'G6"VfGiXIL!CdZX(UAHAS5(Kp64UbAi4a c4+p#$k590!ai&[ecCTqicj[Yh,$Z#'FeA9K9EC0d1hRfmP#cGdC+H0iNM*DLHef c1Tk)@!bPAP[FY[)6iaf9)0AVJ8jd`@@P)j4CC5D)C0I""&"fd`I%Sr3%5'(f(GZ 4#+G$%,aBdhbE!#,)1X!Q"q6rIX`f94EqQJ1#$",ZFRE#2)-#VId52,3@6B$Fblb TA%+Pq,!el5@+Z(cC31Y-1kcaGaeLKT9l'BMXLQK)*b--D1+De,5S@S$BIU&Je@R kd5@A!,0V*4acJVJdY05pCBf)MkXEAVYM26RaD'kBce2&,ID3!'Fbkem#iE(14L8 @Ud,S6j[YJKCBdaj+c4p0p4MIZeha4"j8EeUUQJUE*NLc,Mk()4k@YjQ$H("Y1jX TcFUmNGicr!Ack8+-,L*NKLEXd!laQ@d%cjK(&8IhCd5mi3!*q1q8BZK`ddeiN!" Je'eLI`q%f'A)NAf,DTFPEM&j9i'hKElK@MDQCjMN["jTH4rTR23NaMpQC5jr`Lm 'D`"25X$@phH@2&clr-V,0[EM)6X#&alJA!rVfB`04rq4I,!*r(XJmdG)Jac!5+L KAET-i$rq)BTT@KQi5@e#S(8,6"A)%f,#*"#8!JR#YVq[VeQ&5p3ZY'rS`kh2`[m N'QY1JG28QcS0`S+Qm'Zd`dUre0LD1%6U#N)94[e#($Mql-Q%L#2PEa25jP3pGF3 0KM8dC,JpREUE+Ra%BfV"b(T$@lIpqj9KJ)DpLklm#X+he#BRNAi$Md%'H[XNIdX Tq146b'2MG9E-F4XL%mr([J2"VA8@mV)DEYk8PBHII0-@aKN346pNL+1ZH-IerYr 0Emie[H%aYF"6abRFji'RcZa88j)[,dBUVALdI!f"'4lriFZ#qBXe%MF1a6a`I)T E-`YZE5+mf9Xjc[fM+L[Y`+E60UMZAp688jPJ02apmNdErE68$m-VSjqf*pK*rcp Uj#Pdkj&4I(%kdLh%&+,6L'D5S*Q8[FM9Z!hq&fiCA)"fSk5MI-*HhpD,%q-Y5YN M0ai@B5hL$5"hHJ(`RUpfYVqN1a9V5YE4+!-4JCVMZ6XeV#X"cV*ZrL*+8E!Ur8% a"jPGGUkCq1DDlN*p[PP+cHb[VGNAdeLZV4"T(lYC!%8S@pJ"4HZ&H@@VB4!!*SB ePBI!@pFkp%cb[4dVeV*lHEh$cL2B`f02*8HA`d1rpfDN#6l6MVV0`c+L-VVVIa[ l)iHAGpaZlV'q4&-Jeka&JRpFa&'ZdY"-"e(Pc%[2SaPS$hU1KfhK0'[eXAm4ar! f*kNd$S*eV03J$9TGTD4J)ScB)p'hDddC$QmifV!IE1J*d,a+(di4H1*pC+Pi0PT &a#cBPk[P-YbhKMPr,L@QMq*SmM'++jlXeY@+bAGq2ATaL)da"[`a$UhhC"rHm#U IJ8D-01$*B'"&bCFl4!5leFjpCGqVV@i!QeI%C#%d*p#SK8m@Glk5cXrR*qj2QLp j*",IPB&lCdLd-$qp!k6M[i*,maU[+J8809J'Q#2pMeP[i4,[VpH(!@)F"'!@(N- VBT!!3eHMdIrKZFa@ra&4[PTf6Y5cG9(cCafBRTCm$5bAc1,3Xl,8i6*KAGTh)+I bk@iTL#p(Qra[0fPNI%`m'L1K)-EbiBEJc-S*!Rd9cEJBhB@&YjD&D@&%blf,m[8 CZlF`&0b[cqBYR2$cPl)@ia'9N!#fqF4D@'N(68p,Emf,hAa#[jr2-LpbjA2#(QG "`N,j)b@j9kN95EYhYi!)S(&,kh6rlfPbhM&(A0Z5r5m48JQTT#NcJCkK1V!ID"q Mm12H2p`NFAS22LB[M)eE(C-b+'jN$-'(P&T$V*+0Uk**Kj6H9Df+S&YpAA2ZGkK DKmLp8-HAAY5CZCi!#Z-Fm"%bbEKLIY%k8iD5MlJb*KrM0Ni"`33+MkJ0f`6H5kK !%!0)'kR[TMH4$Lm[mL"F-UXXP,+iLdjXS4#E*CB"CX5Dr@h8ed5d')MLDYZa*#c C1d1pHqaV0!*`D*r`9Z8I$-5p96DFlrIaMUGYk-!9#I)L4)b813"XbLZR-k(lMe" L%"iDGNkA8$+4FIAR1[@XhpYH2a,H92d'cDf[(Q&Jc0PZE0Db2K-)#$YG"",3TM- +(P*m2`##!5f"1p0%$EZ*[J-1)(PMKkqaBc4Bb$AN$`[mYFDJ4JGa`BUfL2%XK#e -Srm9,RBV5%J8HAi3Na55l-I3B6VLXVI`"ZBHFU638cDp6ei%dllKRkXQPkCqei- U)+qb)8i26SeK`[LK!0Zprm'ApjU1%i+[SGHi9U,ECQRG0j4G!8@pRq#+XMj86Ak 095c')pi6Bebbamk0NbU1DB-iH(DrCMZ$&Amq!#cpHVb,TFJMX#qEjK@pafje!6( -rKT%fd0JTUcAVrT5RN,,RP@'&"B8d5U6!d*PMS)UrmFA,ZIjZZq,#)lbr8#N,hK d'&)`I'MAaj!!NF3qPSK9#Z0U''E%C++(G!#VEYZ&8#LVIf[cBJ-2!@0"YIaLJ'% 1)-GYMJ+XA6qMX*Nbj5BGca[Q8p'fV&)VbAJ'&D)86Sf(Nj+[m#&D9Nc[jSIqBpI Z&kjNlGbR@fq4RmN)HDZQ"U`D$6AkLEc!GYIeGN1`jcr%jkMA3ca,'SM-G3f&-dR (`XP$5a0dYMafPblrDDl3LDmja5FRVRK-X1Q)L(rM&Qj,QH)XZTVXl`3KJZ%4pQF d9fmk@+`S'9c*8MQ!Z1jq(qSmPGANTSVpGXRG4N$d%)FYl1CG"#@INeQkheS%(JS C%Kij&EB!G2NM-E%I4-1*l!Ph9,"-[E$4-%`G0#'-H2e3*c5+acbj'"$[JXpSI4D 6d3C*Rj13!!1pFRb)Hmrm&P`NNh0#PjB-1,pP#B(HGXSPra8Uh'khl'X8FE8pD5K VQ+ZR&1kM1&U[Fdh6Ff(R&2DGP#KmE"haI6X(Y1%b+YSYGbV4r&[JM'mG*,X5P1i MM'Gd0Y*ZRRLeZM[6*Pmh8ePRLmN)X,`k8jlaLSEc*L*MVBmQelcDZh([SEF$b23 E-I#p!P1*#Ib0r%lR!V[&l@+A4PdLP`UTIFX%Bc66RT(``%r"!aZHB5i[@!U,lL# 1S)L60(4!e1U68Ll(53)dNeDbLPS)VM3XUqLLTJY[&lABi'3D#[j,%fRHrFf$i1D drm+c#UIAG&[SY0LJc5+%I%'1&h+D'[qU`-ARrjJ!ckj$*@3[&U6ckb#bXJ5ZQ4V d0ckaJp3)Zj!!rq2&#"m6%ZT)#RFN*)`D,cTC@8aGLrF3Zc)f*m5JdmZ"0p5UPfh GUP-Mb[Df,MPT)`-)V1#EKiHA,[HBLjkZP@JJd9U,-GC@@kRI6Ci!SRY@cE%-bT[ l!jj4XXP4XBVL0TCYHBZNVYN%@Q(c',q!l'(`DPTqU"TKdeRf2lENdb4%j#"NZcP lY3M*eNjEAa*AfMNGZ*r$!PYmYRL-lJMdXH#'[+@V'qpZPS'!h%m29ZCCGB1(i+C j`V8dL5eaNB!*$$GDI1DQq88+`C)aPKUDV4$eSM`cMIp,5$kEl1-YHAF8CbQ80'U -XY-8(J@CMKJcJ)c-8[%pe3AE#A)5C!Z3!!VNGC-GkFc6lDAiPipc6'+,MTDhmmU pT6qqUSYSDGMaC-S)pqdZaZ)`LUjI[&-)%j+E`(*3[CPPLcpXa&%f+q1V#6*!GUR Vh'DU@UN4IdJ2"`4,r%2CH`1CNTd@-&%eGQTE,k4pM8XmRIYdHkRehV!DB&-fjEY FeP`HC11BKJiE@8EfU&X*&-3r[e8m"mF(V%Y9(0ZZ1aZ0mD+8K(6R9KLkK`h+Aj! !*d1HrY*p@$A@GS6jA45UCASPfSNqUeVGB'kSk!V(*)42Y+3"`R)TME9V(lp@lQb %JV%%9jK35CjEXiS+&#ZcRLQB2Y%EqB0D2V'McU3jCq')8!"hRf6cZpPr`C'[1aF C3#&YB)&a0k1NmdM)kDiY5[2K*+"8Y`kL(8Y2c4iTfj,Ph%Z'kRm9(YkIJQKV4BA led9D*MZ0%8aY4@T%,Ljf@#c-E9#1dN[bE"k+[X6"bP,P@ZCZ@4*V'VEc)jRpL[d ,reQFLiN!1&L`[ZXa6kNHLIik)aVG5@#0%pFbVN#3!+hN3LXVE6CFe)!IdXJpZiB 8N[4+[5@$`N9m%@ifEL2Nrj5RX+A@(1`P1dqCpAppBF!f[9fYD`"8m'9DQ(T3HJ` X%"b,iUVHGVe`M"-#iX8Zbc3VCZH,ip%CRYB6ULLlQ-eb9!Z1i(&qBPC-kaFb@U8 #kl-aLDm5AeEQ&U&bK8*JKR3p3rZie#&DF4EIiTJ[l%Z"+Ur$%[6Yj83Zq*R8MY' `*-bqfUdN3l#93r3P1bDlSY-"6@jZq86D1irYfDdpVCR%66&&@SpaBq4"h@YX`1B 8aHZ1mJEQhMK'Mc`A#,JHRm8Z"(18cI&l"@c"AD,&'#%lQ`$9pNaaqM)r6FqY+GR pk6U9bETkci0fDNebIbT"!a9ZF!'R6"pcVAQMR[Hd#3*iNG8QeB1pJEHYC,aA*Sl R!mF148m"8kkG8q"2!LkP(diPBae,U(KNPB[L4TC&kf1R8`MIPf!KZiaMk(LVN!" Ld2%3L99(f`[NSJlaEjRQma[QfRpD3FkFF)8$JL(`hdC'+P)9'9p)NiP9Q25Fc5R F02hJc9p'-mJ,#ZH-kTJhc)hN8qidM%46!+2+dk9XP%mmYr6HRQ%"a11&SlSYcae IP%(5deX!PNZQ!rD8+QY%F`j03(j+93(CQBIG+3*rqK[3j"U55#X$a&LB!,MUFQE %EKlqQKM$6B4XflRVD4QdK1Y9DQ8)*1iN5eBReNV+8C(aVa@)8(UklTkRGbTR*Pq M5m,"p+V[hj`j"+ZkdU2BYeU#3C*$f39625mMV4q8AE`[(12[5h!,MZ3,mVl'%Gd qmSb1Z)&QZYdcV,#*cf+8Y6[b`Z!Gj2Y@L)KJcfI*6cS89E&bhPbZ*D(TK6$a0bj lP+LKlcHESXA1EVC@CP@T0R`(2c1N-+)SXb*4BJ1%CL!iaI1IZj1`8i+5%biVK,Y EmTTE`k2B%39qf"iSN[1cd%88Ib`QENFeklN1rP8+Ua+pjqFA[B$K1F@XkNPkc1i 2FJS[2AaK*$6DJD3L-+-iPap+&Fc5cbcMHqf6(hrQ+Pb"U-pX#52D1MQPCZEAX0$ afZjQ5j@dNLN5fi%$P+dbH'N%YGGc')%R8hd`%h2b5dKcjd#EP-@@e#1L4!XYZ1l 2ShB`&R5+YP8)'D1r*D!k(E3!MV&qHI%Y8GEBlF-QV3p[P2eAEl8GF1bfYQ+,J&h GP6GmpS$@9rNZIrHVLC3$S&'J4,Bmk$8[0pD6P+9a'kBL0'H(P"lMGlCiUma"$I9 FiE)ccURY@-)p+XAHi3#(hpTrmKDi)(kMHF(IUh@#8ZV[L"2!66`@LANS-*)&jjl Aq3U9)TEbbIF%YZCdPbH6JAFd3[6-YmHQ`hNEE4@2[hMANBJd9C9leH-#3,P!`%V A*Tbr5Rqh(,,[U(DS!A1EMq6H@6@1&ThM[B9$CDcVUhNPYI!`L#5)NFb)qelej-" 1dXI'D(fCAZE2ZiqDRbZ1DV&2LA2"BL#a91Vq,mII+R8'EA#d*4Y5BBa)&Z4jr6C M@8#9#F5i%%fV1MRpLKGa5$TL!P$['Y"N[SmF299-DeD9GJaRAHf1AV-MSKV!&aj `dk+VqQGkmS,rQN3dURmc6eEAr$LaQkl`qh1H"hhkQj9NEPHV[b43CmDMm&a(U$- BIQ6QYNLfI#9Y6la@ciL#@U%qf@TPHVdZqi4&LDD1'Flq4c6)RNVFRJ)r-FIC85h 3&a*@Z&lBl`mSm1(2,'BjUmSALaV,RQC5)LKLc)6+%PAVi(PeE!M1G@0*Za'#If$ cU'Z+D'+T+`C$eafIr2kZ4S%kp,Gi'(bjkT!!0PdJGJdD-YZ,MY!lM#S'rN@k!5p "ASjbb0J)q!D&`Rf(%f+KaM4EViR&JL-&FJEXR5BBMbZD"$@5[3I00l@+i#U9cdB [c@V2FD'@lA(@PDR58bhr$6b+RADY"TZH3&5bA86*F*`"FQJ&%eBH!8De$DT[Y+h $3KaqQ`4dHX%&0hrdSTRkDP"b1ZPI@-9'k!*6!cd&&Z,ZHR@A19'S#X(9Hidc6B( `SKGej8Di949C(*!!HN-pM2B9qqaYe*6Hf42,8+"c,!X-hXaBpbQApMe[%qRIie$ $a66RXkUR*K)SrUA($)H(1&hV3J3!f[81k"B2a"%*N!"VFmYBAVTP3E0[PHhK2r5 R%hFZQiSATUE)DkTqXGU2#Aq@ZEk3!-Y&d1mU%d"2ThZ*"GP9I"l+VB%[j86HJP4 3'6N'FS*`BF0D3V%5VG+%+e3,,4b1'mSUKE%*M'E&9S9Q`k%+UXblZZRJ43A''qQ hb'jfVD#8q1Epe!U'#3Q"lGd4Zql,FBXQU!3AlhX0JF1!jUY@Zi)h5TpkdMX2HQL k1*b([F`H#[N3&SiBTU[cC'L*@[3iZSpXNiRA+F3aI1UXrl5qT@Ap9-KS`Q2fq%N -*0apKDMDK@A(@HJJ@"9Y59%8eEf0!Ufp"!FVKpTCFba4'VM%`MR0L)BRLkj9ZN4 XV*FK%&%AHhj1NFlr[E%DPKF%p2TaXKrY23"8k"TM0Y1G2$La5hefa8FXS0c[mD[ J[4-hBYkPT29D%#fTM)f"UHq-ebVJeD4f52S36!rdQ%@6NXN$5Q['c*@#eHUD*&f M&SZSf4j,AQLiHB+'[H15Ydm8)Uh-S3Dl)&J+Nf3X+!2Xfa5'5Q3rGi(H2qKpjE` Cd&L2'4DdN9H&H#YrqBT+H1f(8["*mrlCkF-361m5pU2lbmF4l1&XQEVd4m%YB$K ZG[UX68&U(5Jr#RB+AlDXYam14$2TBqMpb9CVj&B''6F0b*FQJdBTa"(%1`)YQX6 XCLep&UU#e(bMjZG*2q9GbUPZZRdJ!@-p!VBfE5@B"JM0"Pb9h*6'#c+A(G%)U`@ &L-4+K&1RKP3C1hZ9A@JH4X#CCjJaQU[@flGU&B-XPD)6dP0rG`dh%,,"6Sp-U*U ,c&YmdGj`,B3"D"cD)Ci&cj(Z0#*j!,,1cmMk@&#+5hAqdJ)CdZ(rD98jHRqr$B2 E6f,dJ8PIk2Akrj9N2@MNhRm-$T!!k+$0'6,8&rCpe[J%prIi2C+JL`3`[j'ik$5 J,iAkRaV(")iimEC0"hD""i3cN!$5JZ(f*%@&,YQ-dZ)YJ%"NS"&-pqTY1Rk0`Up @+"8F(2(ZQZf%f1$,[ZNc53AQ%ZhM$R)mE3U[aYKZr1k$Y3N9d$"5-cE!B6qAQ", l++$EPm(&j"6(QrJmejmD*S5ZPV)#F[6%EG8@!A4D#IFAdHT`h`KhZbNCG8(G4kl 3SIa(f[aX5Q-*&ME[DqeJ0l!D14$3"U2kbBC$RVfB-V('SeIT*H"T,&h5q46rdIl KRAVcXPD!dkCq*'C!L5`29&C2Fk3PMe[LNR)a3S3kCT!!HC+hT`5)r3ld'+(`5L- cmQGQS@@-%aDPp,SpBpHCSqTJS'iUa+V5A![4I6J*Y&cp!H0dC8bA+XTK*im#YA` a6kBrH+%Chb%NriG$cZIX82T!FPj6%p0hG&K'Y&8m803!l,cjGrcdIBhMbKi2rXl [2ZkHA5S[fH`"iPfmTQ`R44e[U+B#f$Kd(50Trkb3!19NmNrk,HFe2kCiX"bE&(Y k+Y-iMH+$Y@I@VL5EC"0'G`Fi--"KcJ90lK66(%S,0LE+P-`Ik4V-[SLdK8*'-fH B3S!qNi)q5h3%M(1#4%M&q%F@64UF[8CIcBHVT%*"QY&p-HcRq4T-fK[[pVK)CB4 YR*[5D)d8#6e[3N%M52kjDf8NkUKS*e((%#hjK3a+ea1XIb4Ia,!U0BUJfU0%CSI Z2jNS+h@EFXZf`3MP8rAUKbFmd`aiMQ[(B!k(K0,q#0i$G0jf+-@1'b`%c'G)KH[ RTK1j#+eB`'r9)G#U%B2KBUejG12jI9C2Jk(['AVjpeLPLfpNfi`+T3f(KmqhriG X%S`GCJRR%`bC&#$e"r"4!c!hS[[R6)4%D45k#EfASHeLT[kaV0EK8*`@a!T6-2H "3Xe,prEX0K@YkNkGj+(+6E[+FLTlBHYH6KFZ&Y+20+RKQ`GdiPJI("9'cm"K8mM (`i%-P'"99J5ETZ3@+L)RNASeaUN(Z1SY"-8`[i1h'aDhH"ZrEErdkJRlTV$iEKf Q&!N5UEYP(#IFBUc%prb[(Xjc+G3HfX#ABU$3!T@0cJrAU6-Q41rXT#VlRL`lA`e qkr8i9f6Xcj+Z9[Ie+XS-'8eifL)RR(jmjR!#mbV"hKbhBA$[R-kd`A+,S&SI#+P MZY01"Ar$9*!!q-G!mpiR@U%SF+Z(Emc1YkNL$p&4rG8EXDC6#UpPB"V8'AKAjEj G1rkVF,ZkVGkd,T@@Ym8)cP&U$cKHG!B@#Brq&3GDqhh)(k#I$YfLKlHlZkliNLN 1C"'I'8941jP(DC3dKBL0[l!b9k0P$BlF(0QQ$6'JXS0[K[m*Bk8R8$1I@[#+@%S @`CdlX(dpiihT[1'eUim#Rb,#AM`QN`,X'$$k46IH))P1EN88CTG`mKQ3!#P,Gq9 '0%&VP*BE&CaeD0CE%%10P"3m@b!h[&iBBYp`VM%04k6JY(P5fH8HK$2hQA&U*!J pk#UXJrl+EZFCZr6fSLqipc(,3P(BPMdk(l0!B'a0qJX2(Np&UJPb9,(Xlh#l"kj @`YYm3#NDkS9(pEM-(FALd*PN*%B+60`qGFjFTe-K$dZIXkaRkf(&BFK0rcPBjL# d&rhbG!8,AM[Z+)icRQB('pN)k#N2A9NQf2h@YeJ2kdrApik4la#jb)FMJAEC*r! CarcYaB`P&YKkrmRQI2BKcJMl513NNYhf1!(L0Y'aDa6F&Y8,$[B8KC3pBR+3!%% `$VcaE6hHqp%F!Y1M@4kJ2j*'1'UP2k0`ppQp"6+&&"GLfE9jDBPF(C9Plq'JKNT +$bI,[Q)F#34M@`Si+)k69B9G`"("P92'9'di6McFU"%4l'9Ur-kZXq'IKAfNm3@ B2IiT`iiCL#Fdjjef)%A*1baSaP5aYMlEiYS4+#Qk(AN+%Q%`$+P380)JBX0dT4( -[da*+HaEA%GMNCi8b(pXij9+A0j'RZG05k*MS-`m!+8Tmj2a6*5T&-8SFf`-%'S CYpZVpZcjAe9TKYa*-V"q'SZ2#q#SR31M!"JYBHk&YQ2)GA%F5J`fU8Y)SZl*UkV 0L+Y"("pD+QFE1H'5)&+$#XbVHiMKZQN6")AL6UG16i)-XQ#&ePVm4aFJ9d9&*-E e8pY9aDJm"i9AY0Pk59FDChCdBq+RHiYB6Te8&NFCZ-"(iY01"+l&lKaNYh!kYpe aFN65Aj8M(D%$196HNhaG1ScqK3IRXDGF&&CeXM"lBdS$Pf"2P1I+0Lc2RX(*[*i ,$P0+eA"LHr*6#&M$d@`%C1RXF0jG9U3l$HH0%L[4h"P4kf[KB0Hq4q3m2h[akHK GjBA(9)4)1E+,H[K`R[Ek(fBE`$Aq-9@&mG3!$'b,D*FKT(#j$H&YKmJ"r[lclKp 2Y!!Q%q,fC"h5lQ%4,UJmP1A+iMGId9@fS9Y9)lS62qa93G#BGFL4CX!(IlIlrJb 9Fh-("!NEUqKiC!Tqp8SmcLS6ATZUi3"cfd"XE@Q%`*,P18[M8J&,Z8leq0@j0lD j#JDp!)&ahRZ&)*e4SR"&9eDKGfDYjirrVU0IRl`4(X4U+DEiL+2Fi`Yh95-U'Si QZ[3JGbG$q6+21F0fm#M3-Jp%!Cm!kT!!`ce%lVjacf2E(S'A(epJ$DX*Y0a[`), mIhPr'RrSIVBm)6!qEGj+,m+9M@-&[%"!php$bXS(KfZlJVBMYjR$4@0Sl"1F4HZ X9P[X2Xcm&H6ZV0)A&b2'h6CSV(,2mdhENl0!N!!1#$TY6,UJj+8"1+aHR01iJp! a2[l2@-GD[@e$I,Jrc!"l59qY'Y-N([kClAiGeID6$@k5['J5d$fb,UV*'G0Kp4Y !f4$@de[5h6")V"pIFV'2&pCc$8mI3`$BeeeiSMIm0PYT#04YbrId[A4EdM8AB4h -M@YCklIE'V3"bf8f*S0hDkT%$GCIkYi`i%m48bX)UH@EaeEEi[GqXX4ES"AGGB& djf$l(4k@)4aTKjIAF+(+dfqfLX&j@lV'Khf-V5VC+1KEkPcKVGj5bB6m'GlG-qf eCM+Lr&3BSU0)25+9qhbP*Z2q21KE8$!A!60S+5E)jDrkl3RdP,L5H$',T0&AfVS [MT'YU9cLS1%%FLCBUL5fdmZFY0#NB(f%90N+kR0`$$8("%-rXVc1MY-qVRm,(R@ D-j8[Kr)FV+PQCS0KlB%9M%T@(4@Q`Xm+kZpP35A`kH(9Hhc")FF4CNc,I`3c36F ,J"`PjfY+-H&NKM[jchAKj+H3!#amU"'p0'U%fLAe$,XJjKS%)KRN@0famZTlSjd b#1fR&&kMLkUeFZVTe1L21jeSE0`DE[PP1aR38bAXH+b"TKHZjfkGQ8VUY@9ZSk6 G"983LN06SH5MEK!@-aa'4+U!D,,0T!mk1FQ,(Ab3!#rdC&`81LV8NfmVr&Ea2AX ![rM`ZkMXLRbULVV#S3q"PUkmY,(eFYV1TT2*9!&`r,iJI0*&eJ(Ip4RU%&9A)eD !4IhQ!90)Eq0hJN"Q2#Yb0)ldVKNQJ-NN3(hHDQL'UTdP[kJGrNIHXf!-HqIB$+4 B0m1G[q"e@mdh`M2hUJS&0e4MZX(bABAUjBLB`a2(IFikiB%Y-qZ@a`8NI"(4E0d #[D8jTkD`F-0k*[YfHe5&[K1V`RlS+KbH(&"*ClHjj"-Dj!*LBB[A1lZ4EYZD,Ml )ZMC66Ia)[!(,9[a1`Gb,4Z*"Q@2J&E'(!)S`@e'K+@Um`hfZSCGhFrc(5&A5'Ah 0XUA#EKD2Z$"kk25r+*elk"HGlZ%KhdU6%S(&"HKPXdBHd4B&&@(L[!QJipk5+V& [*&(Ah$BdMpM5b"X9ppiG(P12CYf9(JT+'5!TSTp-Gcq,!DXldahUQX%c[+*k@*M qC@lqA4Rj&R'8S6lK"AX1Jk9KIlA+N!#Z[N,8$,"l(2`eYVc-E`AB(qrUb'!"BEP fP)FQbe`+*)q!JDP,SXh"2&N'4(TiaIkNqeM`#CP(*DD[Qp5@e*FZ#am)*6[dRU, '`K-B9+qJ6Y,8elY8a+$cLFb8Crcl-&K'PHD2ENJE#K$S+4%pjeYRjTXT1+SE'0H 46F'hAABKXGdP`Q*@'r(GhEZ`T6L'[HlP,MqqDVBU1HMa6FkKRCmDTbqp3A5QImq (2bK-L6XG[EcI5LeL`QCfQq9E3L2VT60H4$"Ap5N[9mMjhZ*'@lj!dmqNfhNZLjC %a2bVcj`hrf3Y8J04DYNMCH'""E[h3NC"e8$C6AdNrc`+(6K`8UJk`993-!kj68B K3%DJMP9jkj(!1LFY@Q)+j*MZ8Xi!U8(Z%#FVRI2PmEkm$iClJTM)B!BFAiAN(UC Lm+"&[5lIhMBYpkqT@4m'CdQ-eCRU9l&N[+H5GjK-``6m!NM6Fid6FDEhjdF!j)6 eC'2SYck5%i'j9h"khj!!Z%f%mN*"KXP"!+KdKQ@44M,D40$9%,3CHJGe&NcFN!" `"Q)S&1bGT4[!Cl3e0%m%Q9J!YVP0,c2-#!,1mr5"rdS02946k3r!phaMIdDUGrr 3PQ$9e!X#-dYR'H*&aJUCHVL8S'h8CT!!)@i!iEa,pJ8AS*(5TVp1SdirlTlc#)` KE#6A@CqLHZc-H'$&LM'2AZ'$9RD0NqV-I65`lhjiUA(hB[L3!"04ekrUJBCQG$D cP#)T@aKpf4!qNmdQk#L'l*NbrHLkpc$4&&(MIHp%CK$`!iTmB[YdkLi(d',)I0* YJ+QjU6Biq5k%[i&NmRfDaGdp5M)lh(Ma#ajHb%mlPY$*SFdej'IMc,"DBTeGdDN #QZ6K5r(IUY%35(f21DpFk#X+)2%,ZMYIBRB%bRG4mMIR-S9(eTfA&2Z-4H9%3[c *%K-%EpF!Tj@*4fl1'"H[(#XUQ8GH@+CM!fC1&kk-r09jN!!F16f%JQqmJaP*GAL F%"L`E88B'X0T`BX10H3Zfk-iZrk5QE#h(#e6H#LUjUj3U6&QT3T,dFUk$CmA,'# mXebXBfZj5LU3!)HmL%d-!A$"2FJB"3[mF&Ij3b%-'Q6+pXcQ@RM3KpLCcZ%-@Ye )@q-IVU,-94Z19$XX#ekMbBaf$DJ2b4KbI+iKr'q0Z"NNl+Lh6J*qaTcB@AG#0XF 56q0""jV1+Q4,N!!AfPQAF6Rfdl2V*+1TiPLd6jVd%MJdf2b63A1,&[BM9rf[L`S XG)+&A)RYhaGj6+!0d%ejF6A+QCU2IV)ij&iTNTI%fpib8CdAULd1rZZB$h'r1k` heKf04la[j5NUA(XF1H25J$kmLNF+Tab%Vhq4k#(d[+(ek1I-955"L-,X!0#F`$j Zj-DATr-DZ5501#N#TfemP`1ITY3[lT(G&!0iDHqMCjTGU,KX04bkHIKaK23[UGZ NbjUF6aQ(*ZYB"$IEbA!Lp0f@3AirNq5U21+3!&ZI#`[c#96G!NUdl%NY6aLmb!5 DaAd%!VqY9rU')a%jqlCl,B!55cMX&Z,[ibB"Q%IP[1JX(qa)--1k-@UV,jL3!(' &B62bT%DXqkb1(MXdLS*3(XHN2P#SPPj8r8rL`DQe)Dj1B@+-2F1-&eD*jL-S!ra 9UXQNlXeX%A@)V2f,G2l+4LC))#I(ZLMN)TRLh)QR&)@"kmD-Ni`EqT[&iB9ekIM adZ'1qEp-*)QjYA4f(Y8BM@ZE5bQ$LCSbENf68a8pZQ8J10S2M%9JcZDcTD2H3Kj ReDCD0UqdI)()MdCXS4Ub*a!b%5`rISZKM[b%@+'C3N9d3Gd'+)65H*GeCA'Q#aq 5&2pZ(VajNHbdBPY6h@1rA#e2R+1B[IdC`H[1FhP"U2FfE+'m@erJlRdhF0)$U9c 3pGQEFS82ebrLCU!a&*0JcYLSq#c56NMUAlbH%`K"bDk@0E5-jMF$A1r$qi+P)'Y IQQjDQ`RN)j!!R(&5E#PIKKQRK9!Rq#3CF8DZ)!#BCrGTUkpJN!!epaa5*E&!$r0 jrP2DG9A*f(3RRhaFVVRdQ(*L&EBKkp,IF)imL@JY5L5qp*a!d06qaZMM2!I#kHL C043V1fDQ06VaE&@b!AeHZ@k-Hc`H44ehk&+1TX2Q3ma!AH"%9DJ0P+MHRM@[Yjp kfK)kKq!b2,PZ$ULeU#F4'BCeIB)GqahcYEdH8I1Z$B"XK'k@C1Cp#!63K5K$aa, !%NE1k!j&,JeBrrreN88ciqU)IkJda1rI-Fk[+0`GHQYJJI3q0ZSBXc`539c`$,R PXX06,$H`e-40F'6rckH((0,&G)P`cLJJDc(YeihfhF'&DUe0SBHDqFRBh4[8m`U FkhcVDmCLXqXD5hm)F(FiSV&Z8E-ZGPHMN!#ITJMd!4TZS!jr6'N4P4Hm3K'cG2E IE,+jAApah6FP2@A0"CC-Z!R!-RRc2Y#j2-QaP80'pHj@H5DlZk65F6%!"Sm,Z,d `2hFe2'`XL'VVmahcCHZTK4j5["&K#bhGeHCiE,(9q*ipicIjarpKkjjbaV5#jD$ *&Dd!(*aZ19HZ*e(C1r9Xqk,5mPGTDFkY6iG0A(5IjUaSf0+5N!$IG$KmFMVh$@H Aiq%&GqZ,emHZ41N1qUP'Sr&dFP`F25Q2pU#BK'G"f3f1*qG4S$Ej8ij0hhAiZT8 r08[k'lJZHJXbUj&+P[0KYLb"T6H0#53eBpbkX8k"fB8I2bS&lB)&Q34GJ$Qj!!!: \ No newline at end of file diff --git a/MacstodonConstants.py b/MacstodonConstants.py index e96075e..57e1c47 100644 --- a/MacstodonConstants.py +++ b/MacstodonConstants.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ######### # Constants # ######### DEBUG = 0 VERSION = "1.1" \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ######### # Constants # ######### DEBUG = 0 VERSION = "1.1.1" \ No newline at end of file diff --git a/MacstodonHelpers.py b/MacstodonHelpers.py index 32aac52..d2b0802 100644 --- a/MacstodonHelpers.py +++ b/MacstodonHelpers.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import EasyDialogs import Fm import formatter import htmllib import ic import Lists import macfs import os import Qd import QuickDraw import string import TE import time import urllib import urlparse import W from TextEdit import * from Wlists import List # ########## # My Imports # ########## from MacstodonConstants import DEBUG, VERSION # ################### # Third-Party Imports # ################### from third_party.PixMapWrapper import PixMapWrapper # ######### # Functions # ######### def buildTimelinePicker(app): menu = [ ("Home", "home"), ("Local", "local"), ("Federated", "federated"), ("Notifications", "notifications"), ("Bookmarks", "bookmarks"), ("Favourites", "favourites"), ("Private Mentions", "mentions"), ("Hashtag...", "__hashtag"), "-" ] for list in app.lists: menu.append((list["title"], list["id"])) return menu def getFilenameFromURL(url): """ Returns the file name, including extension, from a URL. i.e. http://www.google.ca/foo/bar/baz/asdf.jpg returns "asdf.jpg" """ parsed_url = urlparse.urlparse(url) path = parsed_url[2] file_name = os.path.basename(string.replace(path, "/", ":")) return file_name def cleanUpUnicode(content): """ Do the best we can to manually clean up unicode stuff """ # The text is UTF-8, but is being interpreted as MacRoman. # First step is to convert the extended characters into # MacRoman so that they display correctly. content = string.replace(content, "√Ñ", "Ä") content = string.replace(content, "√Ö", "Å") content = string.replace(content, "√á", "Ç") content = string.replace(content, "√â", "É") content = string.replace(content, "√ë", "Ñ") content = string.replace(content, "√ñ", "Ö") content = string.replace(content, "√ú", "Ü") content = string.replace(content, "√°", "á") content = string.replace(content, "√†", "à") content = string.replace(content, "√¢", "â") content = string.replace(content, "√§", "ä") content = string.replace(content, "√£", "ã") content = string.replace(content, "√•", "å") content = string.replace(content, "√ß", "ç") content = string.replace(content, "√©", "é") content = string.replace(content, "√®", "è") content = string.replace(content, "√™", "ê") content = string.replace(content, "√´", "ë") content = string.replace(content, "√≠", "í") content = string.replace(content, "√¨", "ì") content = string.replace(content, "√Æ", "î") content = string.replace(content, "√Ø", "ï") content = string.replace(content, "√±", "ñ") content = string.replace(content, "√≥", "ó") content = string.replace(content, "√≤", "ò") content = string.replace(content, "√¥", "ô") content = string.replace(content, "√∂", "ö") content = string.replace(content, "√µ", "õ") content = string.replace(content, "√∫", "ú") content = string.replace(content, "√π", "ù") content = string.replace(content, "√ª", "û") content = string.replace(content, "√º", "ü") content = string.replace(content, "‚Ć", "†") content = string.replace(content, "¬∞", "°") content = string.replace(content, "¬¢", "¢") content = string.replace(content, "¬£", "£") content = string.replace(content, "¬ß", "§") content = string.replace(content, "‚Ä¢", "•") content = string.replace(content, "¬∂", "¶") content = string.replace(content, "√ü", "ß") content = string.replace(content, "¬Æ", "®") content = string.replace(content, "¬©", "©") content = string.replace(content, "‚Ñ¢", "™") content = string.replace(content, "¬¥", "´") content = string.replace(content, "¬®", "¨") content = string.replace(content, "‚â†", "≠") content = string.replace(content, "√Ü", "Æ") content = string.replace(content, "√ò", "Ø") content = string.replace(content, "‚àû", "∞") content = string.replace(content, "¬±", "±") content = string.replace(content, "‚â§", "≤") content = string.replace(content, "‚â•", "≥") content = string.replace(content, "¬•", "¥") content = string.replace(content, "¬µ", "µ") content = string.replace(content, "‚àÇ", "∂") content = string.replace(content, "‚àë", "∑") content = string.replace(content, "‚àè", "∏") content = string.replace(content, "œÄ", "π") content = string.replace(content, "‚à´", "∫") content = string.replace(content, "¬™", "ª") content = string.replace(content, "¬∫", "º") content = string.replace(content, "Œ©", "Ω") content = string.replace(content, "√¶", "æ") content = string.replace(content, "√∏", "ø") content = string.replace(content, "¬ø", "¿") content = string.replace(content, "¬°", "¡") content = string.replace(content, "¬¨", "¬") content = string.replace(content, "‚àö", "√") content = string.replace(content, "∆í", "ƒ") content = string.replace(content, "‚âà", "≈") content = string.replace(content, "‚àÜ", "∆") content = string.replace(content, "¬´", "«") content = string.replace(content, "¬ª", "»") content = string.replace(content, "‚Ķ", "…") content = string.replace(content, "√Ä", "À") content = string.replace(content, "√É", "Ã") content = string.replace(content, "√ï", "Õ") content = string.replace(content, "≈í", "Œ") content = string.replace(content, "≈ì", "œ") content = string.replace(content, "‚Äì", "–") content = string.replace(content, "‚Äî", "—") content = string.replace(content, "‚Äú", "“") content = string.replace(content, "‚Äù", "”") content = string.replace(content, "‚Äò", "‘") content = string.replace(content, "‚Äô", "’") content = string.replace(content, "√∑", "÷") content = string.replace(content, "‚óä", "◊") content = string.replace(content, "√ø", "ÿ") content = string.replace(content, "≈∏", "Ÿ") content = string.replace(content, "‚ÅÑ", "⁄") content = string.replace(content, "‚Ǩ", "€") content = string.replace(content, "‚Äπ", "‹") content = string.replace(content, "‚Ä∫", "›") content = string.replace(content, "Ô¨Å", "fi") content = string.replace(content, "Ô¨Ç", "fl") content = string.replace(content, "‚Ä°", "‡") content = string.replace(content, "¬∑", "·") content = string.replace(content, "‚Äö", "‚") content = string.replace(content, "‚Äû", "„") content = string.replace(content, "‚Ä∞", "‰") content = string.replace(content, "√Ç", "Â") content = string.replace(content, "√ä", "Ê") content = string.replace(content, "√Å", "Á") content = string.replace(content, "√ã", "Ë") content = string.replace(content, "√à", "È") content = string.replace(content, "√ç", "Í") content = string.replace(content, "√é", "Î") content = string.replace(content, "√è", "Ï") content = string.replace(content, "√å", "Ì") content = string.replace(content, "√ì", "Ó") content = string.replace(content, "√î", "Ô") content = string.replace(content, "Ô£ø", "") content = string.replace(content, "√í", "Ò") content = string.replace(content, "√ö", "Ú") content = string.replace(content, "√õ", "Û") content = string.replace(content, "√ô", "Ù") content = string.replace(content, "ƒ±", "ı") content = string.replace(content, "ÀÜ", "ˆ") content = string.replace(content, "Àú", "˜") content = string.replace(content, "¬Ø", "¯") content = string.replace(content, "Àò", "˘") content = string.replace(content, "Àô", "˙") content = string.replace(content, "Àö", "˚") content = string.replace(content, "¬∏", "¸") content = string.replace(content, "Àù", "˝") content = string.replace(content, "Àõ", "˛") content = string.replace(content, "Àá", "ˇ") # Next, convert unicode codepoints back into characters content = string.replace(content, "\\u003e", ">") content = string.replace(content, "\\u003c", "<") content = string.replace(content, "\\u0026", "&") # Lastly, convert HTML entities back into characters content = string.replace(content, """, '"') content = string.replace(content, "'", "'") content = string.replace(content, "&", "&") content = string.replace(content, ">", ">") content = string.replace(content, "<", "<") # After conversion, any characters above byte 127 are # outside the MacRoman character set and should be # stripped. for char in content: if ord(char) > 127: content = string.replace(content, char, "") return content def decodeJson(data): """ 'Decode' the JSON by taking the advantage of the fact that it is very similar to a Python dict. This is a terrible hack, and you should never do this anywhere because we're literally eval()ing untrusted data from the 'net. I'm only doing it because it's fast and there's not a lot of other options for parsing JSON data in Python 1.5. """ data = string.replace(data, '":null', '":None') data = string.replace(data, '":false', '":0') data = string.replace(data, '":true', '":1') data = eval(data) return data def dprint(text): """ Prints a string to stdout if and only if DEBUG is true """ if DEBUG: print text def okDialog(text, size=None): """ Draws a modal dialog box with the given text and an OK button to dismiss the dialog. """ if not size: size = (360, 120) window = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) window.label = W.TextBox((10, 10, -10, -40), text) window.ok_btn = W.Button((-80, -30, -10, -10), "OK", window.close) window.setdefaultbutton(window.ok_btn) window.open() def okCancelDialog(text, size=None): """ Draws a modal dialog box with the given text and OK/Cancel buttons. The OK button will close the dialog. The Cancel button will raise an Exception, which the caller is expected to catch. """ if not size: size = (360, 120) global dialogWindow dialogWindow = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) def dialogExceptionCallback(): dialogWindow.close() raise KeyboardInterrupt dialogWindow.label = W.TextBox((10, 10, -10, -40), text) dialogWindow.cancel_btn = W.Button((-160, -30, -90, -10), "Cancel", dialogExceptionCallback) dialogWindow.ok_btn = W.Button((-80, -30, -10, -10), "OK", dialogWindow.close) dialogWindow.setdefaultbutton(dialogWindow.ok_btn) dialogWindow.open() def attachmentsDialog(media_attachments): """ Draws a modal dialog box with Download and Close buttons. Between the text and buttons a list is drawn containing attachments. Clicking on an attachment, then clicking on the Download button will save the contents of the attachment to disk. Clicking the Close button will close the window. """ if len(media_attachments) == 0: okDialog("The selected toot contains no attachments.") return global attachmentsWindow, attachmentsData attachmentsFormatted = [] attachmentsData = [] for attachment in media_attachments: if attachment["description"] is not None: desc = cleanUpUnicode(attachment["description"]) listStr = desc + "\r" else: desc = None listStr = "No description\r" if attachment["type"] == "image": listStr = listStr + "Image, %s" % attachment["meta"]["original"]["size"] elif attachment["type"] == "video": listStr = listStr + "Video, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "gifv": listStr = listStr + "GIFV, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "audio": listStr = listStr + "Audio, %s" % attachment["meta"]["length"] else: listStr = listStr + "Unknown" attachmentsFormatted.append(listStr) attachmentsData.append({"url": attachment["url"], "alt": desc}) attachmentsWindow = W.ModalDialog((360, 240), "Macstodon %s - Attachments" % VERSION) def openAttachmentCallback(): """ Run when the user clicks the Download button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: url = attachmentsData[selected[0]]["url"] default_file_name = getFilenameFromURL(url) fss, ok = macfs.StandardPutFile('Save as:', default_file_name) if not ok: return 1 file_path = fss.as_pathname() file_name = os.path.split(file_path)[-1] urllib.urlretrieve(url, file_path) okDialog("Successfully downloaded '%s'!" % file_name) else: okDialog("Please select an attachment first.") def altTextCallback(): """ Run when the user clicks the Alt Text button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: alt_text = attachmentsData[selected[0]]["alt"] if alt_text is not None: okDialog(alt_text) else: okDialog("This attachment doesn't have any alt text.") else: okDialog("Please select an attachment first.") text = "The following attachments were found in the selected toot. " \ "Click on an attachment in the list, then click on the Download button " \ "to download it to your computer. You can also click on the Alt Text " \ "button to view the attachment's alt text in a dialog." attachmentsWindow.label = W.TextBox((10, 10, -10, -60), text) attachmentsWindow.attachments = TwoLineListWithFlags((10, 76, -10, -42), attachmentsFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) attachmentsWindow.alt_btn = W.Button((-240, -30, -170, -10), "Alt Text", altTextCallback) attachmentsWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", attachmentsWindow.close) attachmentsWindow.download_btn = W.Button((-80, -30, -10, -10), "Download", openAttachmentCallback) attachmentsWindow.setdefaultbutton(attachmentsWindow.close_btn) attachmentsWindow.open() def linksDialog(le): """ Draws a modal dialog box with Open and Close buttons. Between the text and buttons a list is drawn containing links. Clicking on a link, then clicking on the Open button will open the link using the user's web browser. Clicking the Close button will close the window. """ global linksWindow, linksFormatted linksFormatted = [] for desc, url in le.anchors.items(): linksFormatted.append(desc + "\r" + url[0]) if len(linksFormatted) == 0: okDialog("The selected toot contains no links.") return linksWindow = W.ModalDialog((240, 240), "Macstodon %s - Links" % VERSION) def openLinkCallback(): """ Run when the user clicks the Open button """ selected = linksWindow.links.getselection() if len(selected) > 0: linkString = linksFormatted[selected[0]] linkParts = string.split(linkString, "\r") ic.launchurl(linkParts[1]) else: okDialog("Please select a link first.") text = "The following links were found in the selected toot. " \ "Click on a link in the list, then click on the Open button " \ "to open it with your browser." linksWindow.label = W.TextBox((10, 10, -10, -40), text) linksWindow.links = TwoLineListWithFlags((10, 56, -10, -42), linksFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) linksWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", linksWindow.close) linksWindow.open_btn = W.Button((-80, -30, -10, -10), "Open", openLinkCallback) linksWindow.setdefaultbutton(linksWindow.close_btn) linksWindow.open() def handleRequest(app, path, data = None, use_token = 0, title = "Working..."): """ HTTP request wrapper """ try: W.SetCursor("watch") pb = EasyDialogs.ProgressBar(title=title, maxval=3) if data == {}: data = "" elif data: data = urllib.urlencode(data) prefs = app.getprefs() url = "%s%s" % (prefs.server, path) dprint(url) dprint(data) dprint("connecting") pb.label("Connecting...") pb.inc() try: if use_token: urlopener = TokenURLopener(prefs.token) handle = urlopener.open(url, data) else: handle = urllib.urlopen(url, data) except IOError: del pb W.SetCursor("arrow") errmsg = "Unable to open a connection to: %s.\rPlease check that your SSL proxy is working properly and that the URL starts with 'http'." okDialog(errmsg % url) return None except TypeError: del pb W.SetCursor("arrow") errmsg = "The provided URL is malformed: %s.\rPlease check that you have typed the URL correctly." okDialog(errmsg % url) return None dprint("reading http headers") dprint(handle.info()) dprint("reading http body") pb.label("Fetching data...") pb.inc() try: data = handle.read() except IOError: del pb W.SetCursor("arrow") errmsg = "The connection was closed by the remote server while Macstodon was reading data.\rPlease check that your SSL proxy is working properly." okDialog(errmsg) return None try: handle.close() except IOError: pass pb.label("Parsing data...") pb.inc() dprint("parsing response json") try: decoded = decodeJson(data) dprint(decoded) pb.label("Done.") pb.inc() time.sleep(0.5) del pb W.SetCursor("arrow") return decoded except: del pb W.SetCursor("arrow") dprint("ACK! JSON Parsing failure :(") dprint("This is what came back from the server:") dprint(data) okDialog("Error parsing JSON response from the server.") return None except KeyboardInterrupt: # the user pressed cancel in the progress bar window W.SetCursor("arrow") return None def getCurrentUser(app): """ Gets the currently logged in user. """ path = "/api/v1/accounts/verify_credentials" data = handleRequest(app, path, None, use_token=1, title = "Getting User...") if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when getting current user:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting current user:\r\r %s" % data['error']) else: return data def getLists(app): """ Gets the user's lists. """ path = "/api/v1/lists" data = handleRequest(app, path, None, use_token=1, title = "Getting Lists...") if not data: # handleRequest failed and should have popped an error dialog return # if data is a list, it worked if type(data) == type([]): return data if data.get("error_description") is not None: okDialog("Server error when getting lists:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting lists:\r\r %s" % data['error']) else: okDialog("Server error when getting lists. Unable to determine data type.") # ####### # Classes # ####### class ImageWidget(W.ClickableWidget): """ A widget that displays an image. The image should be passed in as a PixMapWrapper. """ def __init__(self, possize, pixmap=None, callback=None): W.ClickableWidget.__init__(self, possize) self._callback = callback self._enabled = 1 # Set initial image self._imgloaded = 0 self._pixmap = None if pixmap: self.setImage(pixmap) def click(self, point, modifiers): """ Runs the callback if the user clicks on the image """ if not self._enabled: return if self._callback: return W.CallbackCall(self._callback, 0) def close(self): """ Destroys the widget and frees up its memory """ W.Widget.close(self) del self._imgloaded del self._pixmap def setImage(self, pixmap): """ Loads a new image into the widget. The image will be automatically scaled to the size of the widget. """ self._pixmap = pixmap self._imgloaded = 1 if self._parentwindow: self.draw() def clearImage(self): """ Unloads the image from the widget without destroying the widget. Use this to make the widget draw an empty square. """ self._imgloaded = 0 Qd.EraseRect(self._bounds) if self._parentwindow: self.draw() self._pixmap = None def draw(self, visRgn = None): """ Draw the image within the widget if it is loaded """ if self._visible: if self._imgloaded: if isinstance(self._pixmap, PixMapWrapper): self._pixmap.blit( x1=self._bounds[0], y1=self._bounds[1], x2=self._bounds[2], y2=self._bounds[3], port=self._parentwindow.wid.GetWindowPort() ) else: Qd.SetPort(self._parentwindow.wid.GetWindowPort()) Qd.DrawPicture(self._pixmap, self._bounds) class LinkExtractor(htmllib.HTMLParser): """ A very basic link extractor that gets the URL and associated text. Sourced from: https://oreilly.com/library/view/python-standard-library/0596000960/ch05s05.html """ def __init__(self, verbose=0): self.anchors = {} f = formatter.NullFormatter() htmllib.HTMLParser.__init__(self, f, verbose) def anchor_bgn(self, href, name, type): self.save_bgn() self.anchor = href def anchor_end(self): text = string.strip(self.save_end()) if self.anchor and text: self.anchors[text] = self.anchors.get(text, []) + [self.anchor] class ProfileBanner(ImageWidget): """ The ProfileBanner is just an ImageWidget that renders text atop it at a fixed location, using a specific font and style. """ def __init__(self, possize, drop_shadow, pixmap=None, display_name="", acct_name=""): ImageWidget.__init__(self, possize, pixmap) self.display_name = display_name self.acct_name = acct_name self.drop_shadow = drop_shadow def draw(self, visRgn = None): """ Draws the profile banner text using pure QuickDraw (instead of widgets) """ Qd.SetPort(self._parentwindow.wid.GetWindowPort()) ImageWidget.draw(self, visRgn) Qd.TextFont(Fm.GetFNum("Geneva")) # Display Name Qd.TextSize(14) Qd.TextFace(QuickDraw.bold) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 70, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 68, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) # Account Name Qd.TextSize(9) Qd.TextFace(QuickDraw.normal) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 90, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 88, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((0,0,0)) Qd.TextFont(Fm.GetFNum("Monaco")) def populate(self, display_name=None, acct_name=None): """ Sets the display and account names. """ if display_name: self.display_name = display_name if acct_name: self.acct_name = acct_name class ProfilePanel(W.Group): """ The ProfilePanel is my poor man's implementation of tabs. It uses three buttons to swap out the widget beneath them. """ def __init__(self, possize): W.Group.__init__(self, possize) self.title = W.TextBox((0, 2, 0, 16), "Bio") self.btnStats = W.Button((-40, 0, 40, 16), "Stats", self.statsCallback) self.btnLinks = W.Button((-90, 0, 40, 16), "Links", self.linksCallback) self.btnBio = W.Button((-140, 0, 40, 16), "Bio", self.bioCallback) # Bio editor = W.EditText((0, 24, -15, 0), "", readonly=1) self._bary = W.Scrollbar((-16, 24, 0, 0), editor.vscroll, max=32767) self.bioText = editor # Stats & Links self.list = TwoLineListWithFlags((0, 24, 0, 0), [], callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) self.linksData = [] self.statsData = [] self.toots = 0 self.following = 0 self.followers = 0 self.locked = "No" self.bot = "No" self.group = "No" self.discoverable = "No" self.noindex = "No" self.moved = "No" self.suspended = "No" self.limited = "No" # hide stats/links by default self.list.show(0) self.bioText.select(0) def statsCallback(self): """ Shows the stats pane and hides bio/links """ self.title.set("Stats") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.statsData) def linksCallback(self): """ Shows the links pane and hides bio/stats """ self.title.set("Links") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.linksData) def bioCallback(self): """ Shows the bio pane and hides stats/links """ self.title.set("Bio") self.list.show(0) self.list.enable(0) self.list.select(0) self.bioText.show(1) self.bioText.enable(1) self.bioText._selectable = 1 self._bary.show(1) self._bary.draw() self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() def setBio(self, value): """ Updates the content of the bio text """ self.bioText.set(value) def collectStatsData(self): """ Updates the content of the stats list """ self.statsData = [ "Toots\r%s" % self.toots, "Following\r%s" % self.following, "Followers\r%s" % self.followers, "Locked\r%s" % self.locked, "Bot\r%s" % self.bot, "Group\r%s" % self.group, "Discoverable\r%s" % self.discoverable, "NoIndex\r%s" % self.noindex, "Moved\r%s" % self.moved, "Suspended\r%s" % self.suspended, "Limited\r%s" % self.limited ] self.list.set(self.statsData) def setLinks(self, linksData): """ Updates the content of the links list """ self.linksData = linksData self.list.set(self.linksData) def setToots(self, num): """ Updates the user's toot count """ self.toots = num self.collectStatsData() def setFollowers(self, num): """ Updates the user's followers count """ self.followers = num self.collectStatsData() def setFollowing(self, num): """ Updates the user's following count """ self.following = num self.collectStatsData() def setLocked(self, flag): if flag: self.locked = "Yes" else: self.locked = "No" self.collectStatsData() def setBot(self, flag): if flag: self.bot = "Yes" else: self.bot = "No" self.collectStatsData() def setDiscoverable(self, flag): if flag: self.discoverable = "Yes" else: self.discoverable = "No" self.collectStatsData() def setNoIndex(self, flag): if flag: self.noindex = "Yes" else: self.noindex = "No" self.collectStatsData() def setMoved(self, flag): if flag: self.moved = "Yes" else: self.moved = "No" self.collectStatsData() def setSuspended(self, flag): if flag: self.suspended = "Yes" else: self.suspended = "No" self.collectStatsData() def setLimited(self, flag): if flag: self.limited = "Yes" else: self.limited = "No" self.collectStatsData() class TitledEditText(W.Group): """ A text edit field with a title and optional scrollbars attached to it. Shamelessly stolen from MacPython's PyEdit. Modified to also allow setting the title, and add scrollbars. """ def __init__(self, possize, title, text="", readonly=0, vscroll=0, hscroll=0): W.Group.__init__(self, possize) self.title = W.TextBox((0, 0, 0, 16), title) if vscroll and hscroll: editor = W.EditText((0, 16, -15, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, -15, 16), editor.hscroll, max=32767) self._bary = W.Scrollbar((-16, 16,0, -15), editor.vscroll, max=32767) elif vscroll: editor = W.EditText((0, 16, -15, 0), text, readonly=readonly) self._bary = W.Scrollbar((-16, 16, 0, 0), editor.vscroll, max=32767) elif hscroll: editor = W.EditText((0, 16, 0, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, 0, 16), editor.hscroll, max=32767) else: editor = W.EditText((0, 16, 0, 0), text, readonly=readonly) self.edit = editor def setTitle(self, value): self.title.set(value) def set(self, value): self.edit.set(value) def get(self): return self.edit.get() class TokenURLopener(urllib.FancyURLopener): """ Extends urllib.FancyURLopener to add the Authorization header with a bearer token. """ def __init__(self, token, *args): apply(urllib.FancyURLopener.__init__, (self,) + args) self.addheaders.append(("Authorization", "Bearer %s" % token)) class TwoLineListWithFlags(List): """ Modification of MacPython's TwoLineList to support flags. """ LDEF_ID = 468 def createlist(self): import List self._calcbounds() self.SetPort() rect = self._bounds rect = rect[0]+1, rect[1]+1, rect[2]-16, rect[3]-1 self._list = List.LNew(rect, (0, 0, 1, 0), (0, 28), self.LDEF_ID, self._parentwindow.wid, 0, 1, 0, 1) self._list.selFlags = self._flags self.set(self.items) class TimelineList(W.Group): """ A TwoLineListWithFlags that also has a title attached to it. Based on TitledEditText. """ def __init__(self, possize, title, items = None, picker = 1, pickerItems = None, btnCallback = None, callback = None, pickerCallback = None, flags = 0, cols = 1, typingcasesens=0): W.Group.__init__(self, possize) if picker: self.listpicker = W.PopupWidget((-68, 0, 16, 16), pickerItems, pickerCallback) self.title = W.TextBox((0, 2, -68, 16), title) self.btn = W.Button((-50, 0, 0, 16), "Refresh", btnCallback) self.list = TwoLineListWithFlags((0, 24, 0, 0), items, callback, flags, cols, typingcasesens) def setListPicker(self, value): self.listpicker.set(value) def setTitle(self, value): self.title.set(value) def set(self, items): self.list.set(items) def get(self): return self.list.items def getselection(self): return self.list.getselection() def setselection(self, selection): return self.list.setselection(selection) \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import EasyDialogs import Fm import formatter import htmllib import ic import Lists import macfs import os import Qd import QuickDraw import string import TE import time import urllib import urlparse import W from TextEdit import * from Wlists import List # ########## # My Imports # ########## from MacstodonConstants import DEBUG, VERSION # ################### # Third-Party Imports # ################### from third_party.PixMapWrapper import PixMapWrapper # ######### # Functions # ######### def buildTimelinePicker(app): menu = [ ("Home", "home"), ("Local", "local"), ("Federated", "federated"), ("Notifications", "notifications"), ("Bookmarks", "bookmarks"), ("Favourites", "favourites"), ("Private Mentions", "mentions"), ("Hashtag...", "__hashtag"), "-" ] for list in app.lists: menu.append((list["title"], list["id"])) return menu def getFilenameFromURL(url): """ Returns the file name, including extension, from a URL. i.e. http://www.google.ca/foo/bar/baz/asdf.jpg returns "asdf.jpg" """ parsed_url = urlparse.urlparse(url) path = parsed_url[2] file_name = os.path.basename(string.replace(path, "/", ":")) return file_name def cleanUpUnicode(content): """ Do the best we can to manually clean up unicode stuff """ # The text is UTF-8, but is being interpreted as MacRoman. # First step is to convert the extended characters into # MacRoman so that they display correctly. content = string.replace(content, "√Ñ", "Ä") content = string.replace(content, "√Ö", "Å") content = string.replace(content, "√á", "Ç") content = string.replace(content, "√â", "É") content = string.replace(content, "√ë", "Ñ") content = string.replace(content, "√ñ", "Ö") content = string.replace(content, "√ú", "Ü") content = string.replace(content, "√°", "á") content = string.replace(content, "√†", "à") content = string.replace(content, "√¢", "â") content = string.replace(content, "√§", "ä") content = string.replace(content, "√£", "ã") content = string.replace(content, "√•", "å") content = string.replace(content, "√ß", "ç") content = string.replace(content, "√©", "é") content = string.replace(content, "√®", "è") content = string.replace(content, "√™", "ê") content = string.replace(content, "√´", "ë") content = string.replace(content, "√≠", "í") content = string.replace(content, "√¨", "ì") content = string.replace(content, "√Æ", "î") content = string.replace(content, "√Ø", "ï") content = string.replace(content, "√±", "ñ") content = string.replace(content, "√≥", "ó") content = string.replace(content, "√≤", "ò") content = string.replace(content, "√¥", "ô") content = string.replace(content, "√∂", "ö") content = string.replace(content, "√µ", "õ") content = string.replace(content, "√∫", "ú") content = string.replace(content, "√π", "ù") content = string.replace(content, "√ª", "û") content = string.replace(content, "√º", "ü") content = string.replace(content, "‚Ć", "†") content = string.replace(content, "¬∞", "°") content = string.replace(content, "¬¢", "¢") content = string.replace(content, "¬£", "£") content = string.replace(content, "¬ß", "§") content = string.replace(content, "‚Ä¢", "•") content = string.replace(content, "¬∂", "¶") content = string.replace(content, "√ü", "ß") content = string.replace(content, "¬Æ", "®") content = string.replace(content, "¬©", "©") content = string.replace(content, "‚Ñ¢", "™") content = string.replace(content, "¬¥", "´") content = string.replace(content, "¬®", "¨") content = string.replace(content, "‚â†", "≠") content = string.replace(content, "√Ü", "Æ") content = string.replace(content, "√ò", "Ø") content = string.replace(content, "‚àû", "∞") content = string.replace(content, "¬±", "±") content = string.replace(content, "‚â§", "≤") content = string.replace(content, "‚â•", "≥") content = string.replace(content, "¬•", "¥") content = string.replace(content, "¬µ", "µ") content = string.replace(content, "‚àÇ", "∂") content = string.replace(content, "‚àë", "∑") content = string.replace(content, "‚àè", "∏") content = string.replace(content, "œÄ", "π") content = string.replace(content, "‚à´", "∫") content = string.replace(content, "¬™", "ª") content = string.replace(content, "¬∫", "º") content = string.replace(content, "Œ©", "Ω") content = string.replace(content, "√¶", "æ") content = string.replace(content, "√∏", "ø") content = string.replace(content, "¬ø", "¿") content = string.replace(content, "¬°", "¡") content = string.replace(content, "¬¨", "¬") content = string.replace(content, "‚àö", "√") content = string.replace(content, "∆í", "ƒ") content = string.replace(content, "‚âà", "≈") content = string.replace(content, "‚àÜ", "∆") content = string.replace(content, "¬´", "«") content = string.replace(content, "¬ª", "»") content = string.replace(content, "‚Ķ", "…") content = string.replace(content, "√Ä", "À") content = string.replace(content, "√É", "Ã") content = string.replace(content, "√ï", "Õ") content = string.replace(content, "≈í", "Œ") content = string.replace(content, "≈ì", "œ") content = string.replace(content, "‚Äì", "–") content = string.replace(content, "‚Äî", "—") content = string.replace(content, "‚Äú", "“") content = string.replace(content, "‚Äù", "”") content = string.replace(content, "‚Äò", "‘") content = string.replace(content, "‚Äô", "’") content = string.replace(content, "√∑", "÷") content = string.replace(content, "‚óä", "◊") content = string.replace(content, "√ø", "ÿ") content = string.replace(content, "≈∏", "Ÿ") content = string.replace(content, "‚ÅÑ", "⁄") content = string.replace(content, "‚Ǩ", "€") content = string.replace(content, "‚Äπ", "‹") content = string.replace(content, "‚Ä∫", "›") content = string.replace(content, "Ô¨Å", "fi") content = string.replace(content, "Ô¨Ç", "fl") content = string.replace(content, "‚Ä°", "‡") content = string.replace(content, "¬∑", "·") content = string.replace(content, "‚Äö", "‚") content = string.replace(content, "‚Äû", "„") content = string.replace(content, "‚Ä∞", "‰") content = string.replace(content, "√Ç", "Â") content = string.replace(content, "√ä", "Ê") content = string.replace(content, "√Å", "Á") content = string.replace(content, "√ã", "Ë") content = string.replace(content, "√à", "È") content = string.replace(content, "√ç", "Í") content = string.replace(content, "√é", "Î") content = string.replace(content, "√è", "Ï") content = string.replace(content, "√å", "Ì") content = string.replace(content, "√ì", "Ó") content = string.replace(content, "√î", "Ô") content = string.replace(content, "Ô£ø", "") content = string.replace(content, "√í", "Ò") content = string.replace(content, "√ö", "Ú") content = string.replace(content, "√õ", "Û") content = string.replace(content, "√ô", "Ù") content = string.replace(content, "ƒ±", "ı") content = string.replace(content, "ÀÜ", "ˆ") content = string.replace(content, "Àú", "˜") content = string.replace(content, "¬Ø", "¯") content = string.replace(content, "Àò", "˘") content = string.replace(content, "Àô", "˙") content = string.replace(content, "Àö", "˚") content = string.replace(content, "¬∏", "¸") content = string.replace(content, "Àù", "˝") content = string.replace(content, "Àõ", "˛") content = string.replace(content, "Àá", "ˇ") # Next, convert unicode codepoints back into characters content = string.replace(content, "\\u003e", ">") content = string.replace(content, "\\u003c", "<") content = string.replace(content, "\\u0026", "&") # Lastly, convert HTML entities back into characters content = string.replace(content, """, '"') content = string.replace(content, "'", "'") content = string.replace(content, "&", "&") content = string.replace(content, ">", ">") content = string.replace(content, "<", "<") # After conversion, any characters above byte 127 are # outside the MacRoman character set and should be # stripped. for char in content: if ord(char) > 127: content = string.replace(content, char, "") return content def decodeJson(data): """ 'Decode' the JSON by taking the advantage of the fact that it is very similar to a Python dict. This is a terrible hack, and you should never do this anywhere because we're literally eval()ing untrusted data from the 'net. I'm only doing it because it's fast and there's not a lot of other options for parsing JSON data in Python 1.5. """ data = string.replace(data, '":null', '":None') data = string.replace(data, '":false', '":0') data = string.replace(data, '":true', '":1') data = eval(data) return data def dprint(text): """ Prints a string to stdout if and only if DEBUG is true """ if DEBUG: print text def okDialog(text, size=None): """ Draws a modal dialog box with the given text and an OK button to dismiss the dialog. """ if not size: size = (360, 120) window = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) window.label = W.TextBox((10, 10, -10, -40), text) window.ok_btn = W.Button((-80, -30, -10, -10), "OK", window.close) window.setdefaultbutton(window.ok_btn) window.open() def okCancelDialog(text, size=None): """ Draws a modal dialog box with the given text and OK/Cancel buttons. The OK button will close the dialog. The Cancel button will raise an Exception, which the caller is expected to catch. """ if not size: size = (360, 120) global dialogWindow dialogWindow = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) def dialogExceptionCallback(): dialogWindow.close() raise KeyboardInterrupt dialogWindow.label = W.TextBox((10, 10, -10, -40), text) dialogWindow.cancel_btn = W.Button((-160, -30, -90, -10), "Cancel", dialogExceptionCallback) dialogWindow.ok_btn = W.Button((-80, -30, -10, -10), "OK", dialogWindow.close) dialogWindow.setdefaultbutton(dialogWindow.ok_btn) dialogWindow.open() def attachmentsDialog(media_attachments): """ Draws a modal dialog box with Download and Close buttons. Between the text and buttons a list is drawn containing attachments. Clicking on an attachment, then clicking on the Download button will save the contents of the attachment to disk. Clicking the Close button will close the window. """ if len(media_attachments) == 0: okDialog("The selected toot contains no attachments.") return global attachmentsWindow, attachmentsData attachmentsFormatted = [] attachmentsData = [] for attachment in media_attachments: if attachment["description"] is not None: desc = cleanUpUnicode(attachment["description"]) listStr = desc + "\r" else: desc = None listStr = "No description\r" if attachment["type"] == "image": listStr = listStr + "Image, %s" % attachment["meta"]["original"]["size"] elif attachment["type"] == "video": listStr = listStr + "Video, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "gifv": listStr = listStr + "GIFV, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "audio": listStr = listStr + "Audio, %s" % attachment["meta"]["length"] else: listStr = listStr + "Unknown" attachmentsFormatted.append(listStr) attachmentsData.append({"url": attachment["url"], "alt": desc}) attachmentsWindow = W.ModalDialog((360, 240), "Macstodon %s - Attachments" % VERSION) def openAttachmentCallback(): """ Run when the user clicks the Download button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: url = attachmentsData[selected[0]]["url"] default_file_name = getFilenameFromURL(url) fss, ok = macfs.StandardPutFile('Save as:', default_file_name) if not ok: return 1 file_path = fss.as_pathname() file_name = os.path.split(file_path)[-1] urllib.urlretrieve(url, file_path) okDialog("Successfully downloaded '%s'!" % file_name) else: okDialog("Please select an attachment first.") def altTextCallback(): """ Run when the user clicks the Alt Text button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: alt_text = attachmentsData[selected[0]]["alt"] if alt_text is not None: okDialog(alt_text) else: okDialog("This attachment doesn't have any alt text.") else: okDialog("Please select an attachment first.") text = "The following attachments were found in the selected toot. " \ "Click on an attachment in the list, then click on the Download button " \ "to download it to your computer. You can also click on the Alt Text " \ "button to view the attachment's alt text in a dialog." attachmentsWindow.label = W.TextBox((10, 10, -10, -60), text) attachmentsWindow.attachments = TwoLineListWithFlags((10, 76, -10, -42), attachmentsFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) attachmentsWindow.alt_btn = W.Button((-240, -30, -170, -10), "Alt Text", altTextCallback) attachmentsWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", attachmentsWindow.close) attachmentsWindow.download_btn = W.Button((-80, -30, -10, -10), "Download", openAttachmentCallback) attachmentsWindow.setdefaultbutton(attachmentsWindow.close_btn) attachmentsWindow.open() def linksDialog(le): """ Draws a modal dialog box with Open and Close buttons. Between the text and buttons a list is drawn containing links. Clicking on a link, then clicking on the Open button will open the link using the user's web browser. Clicking the Close button will close the window. """ global linksWindow, linksFormatted linksFormatted = [] for desc, url in le.anchors.items(): linksFormatted.append(desc + "\r" + url[0]) if len(linksFormatted) == 0: okDialog("The selected toot contains no links.") return linksWindow = W.ModalDialog((240, 240), "Macstodon %s - Links" % VERSION) def openLinkCallback(): """ Run when the user clicks the Open button """ selected = linksWindow.links.getselection() if len(selected) > 0: linkString = linksFormatted[selected[0]] linkParts = string.split(linkString, "\r") ic.launchurl(linkParts[1]) else: okDialog("Please select a link first.") text = "The following links were found in the selected toot. " \ "Click on a link in the list, then click on the Open button " \ "to open it with your browser." linksWindow.label = W.TextBox((10, 10, -10, -40), text) linksWindow.links = TwoLineListWithFlags((10, 56, -10, -42), linksFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) linksWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", linksWindow.close) linksWindow.open_btn = W.Button((-80, -30, -10, -10), "Open", openLinkCallback) linksWindow.setdefaultbutton(linksWindow.close_btn) linksWindow.open() def handleRequest(app, path, data = None, use_token = 0, title = "Working..."): """ HTTP request wrapper """ try: W.SetCursor("watch") pb = EasyDialogs.ProgressBar(title=title, maxval=3) if data == {}: data = "" elif data: data = urllib.urlencode(data) prefs = app.getprefs() url = "%s%s" % (prefs.server, path) dprint(url) dprint(data) dprint("connecting") pb.label("Connecting...") pb.inc() try: if use_token: urlopener = TokenURLopener(prefs.token) handle = urlopener.open(url, data) else: handle = urllib.urlopen(url, data) except IOError: del pb W.SetCursor("arrow") errmsg = "Unable to open a connection to: %s.\rPlease check that your SSL proxy is working properly and that the URL starts with 'http'." okDialog(errmsg % url) return None except TypeError: del pb W.SetCursor("arrow") errmsg = "The provided URL is malformed: %s.\rPlease check that you have typed the URL correctly." okDialog(errmsg % url) return None dprint("reading http headers") dprint(handle.info()) dprint("reading http body") pb.label("Fetching data...") pb.inc() try: data = handle.read() except IOError: del pb W.SetCursor("arrow") errmsg = "The connection was closed by the remote server while Macstodon was reading data.\rPlease check that your SSL proxy is working properly." okDialog(errmsg) return None try: handle.close() except IOError: pass pb.label("Parsing data...") pb.inc() dprint("parsing response json") try: decoded = decodeJson(data) dprint(decoded) pb.label("Done.") pb.inc() time.sleep(0.5) del pb W.SetCursor("arrow") return decoded except: del pb W.SetCursor("arrow") dprint("ACK! JSON Parsing failure :(") dprint("This is what came back from the server:") dprint(data) okDialog("Error parsing JSON response from the server.") return None except KeyboardInterrupt: # the user pressed cancel in the progress bar window W.SetCursor("arrow") return None def getCurrentUser(app): """ Gets the currently logged in user. """ path = "/api/v1/accounts/verify_credentials" data = handleRequest(app, path, None, use_token=1, title = "Getting User...") if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when getting current user:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting current user:\r\r %s" % data['error']) else: return data def getLists(app): """ Gets the user's lists. """ path = "/api/v1/lists" data = handleRequest(app, path, None, use_token=1, title = "Getting Lists...") if data is None: # handleRequest failed and should have popped an error dialog return # if data is a list, it worked if type(data) == type([]): return data # otherwise data is an error dict if data.get("error_description") is not None: okDialog("Server error when getting lists:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting lists:\r\r %s" % data['error']) else: okDialog("Server error when getting lists. Unable to determine data type.") # ####### # Classes # ####### class ImageWidget(W.ClickableWidget): """ A widget that displays an image. The image should be passed in as a PixMapWrapper. """ def __init__(self, possize, pixmap=None, callback=None): W.ClickableWidget.__init__(self, possize) self._callback = callback self._enabled = 1 # Set initial image self._imgloaded = 0 self._pixmap = None if pixmap: self.setImage(pixmap) def click(self, point, modifiers): """ Runs the callback if the user clicks on the image """ if not self._enabled: return if self._callback: return W.CallbackCall(self._callback, 0) def close(self): """ Destroys the widget and frees up its memory """ W.Widget.close(self) del self._imgloaded del self._pixmap def setImage(self, pixmap): """ Loads a new image into the widget. The image will be automatically scaled to the size of the widget. """ self._pixmap = pixmap self._imgloaded = 1 if self._parentwindow: self.draw() def clearImage(self): """ Unloads the image from the widget without destroying the widget. Use this to make the widget draw an empty square. """ self._imgloaded = 0 Qd.EraseRect(self._bounds) if self._parentwindow: self.draw() self._pixmap = None def draw(self, visRgn = None): """ Draw the image within the widget if it is loaded """ if self._visible: if self._imgloaded: if isinstance(self._pixmap, PixMapWrapper): self._pixmap.blit( x1=self._bounds[0], y1=self._bounds[1], x2=self._bounds[2], y2=self._bounds[3], port=self._parentwindow.wid.GetWindowPort() ) else: Qd.SetPort(self._parentwindow.wid.GetWindowPort()) Qd.DrawPicture(self._pixmap, self._bounds) class LinkExtractor(htmllib.HTMLParser): """ A very basic link extractor that gets the URL and associated text. Sourced from: https://oreilly.com/library/view/python-standard-library/0596000960/ch05s05.html """ def __init__(self, verbose=0): self.anchors = {} f = formatter.NullFormatter() htmllib.HTMLParser.__init__(self, f, verbose) def anchor_bgn(self, href, name, type): self.save_bgn() self.anchor = href def anchor_end(self): text = string.strip(self.save_end()) if self.anchor and text: self.anchors[text] = self.anchors.get(text, []) + [self.anchor] class ProfileBanner(ImageWidget): """ The ProfileBanner is just an ImageWidget that renders text atop it at a fixed location, using a specific font and style. """ def __init__(self, possize, drop_shadow, pixmap=None, display_name="", acct_name=""): ImageWidget.__init__(self, possize, pixmap) self.display_name = display_name self.acct_name = acct_name self.drop_shadow = drop_shadow def draw(self, visRgn = None): """ Draws the profile banner text using pure QuickDraw (instead of widgets) """ Qd.SetPort(self._parentwindow.wid.GetWindowPort()) ImageWidget.draw(self, visRgn) Qd.TextFont(Fm.GetFNum("Geneva")) # Display Name Qd.TextSize(14) Qd.TextFace(QuickDraw.bold) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 70, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 68, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) # Account Name Qd.TextSize(9) Qd.TextFace(QuickDraw.normal) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 90, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 88, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((0,0,0)) Qd.TextFont(Fm.GetFNum("Monaco")) def populate(self, display_name=None, acct_name=None): """ Sets the display and account names. """ if display_name: self.display_name = display_name if acct_name: self.acct_name = acct_name class ProfilePanel(W.Group): """ The ProfilePanel is my poor man's implementation of tabs. It uses three buttons to swap out the widget beneath them. """ def __init__(self, possize): W.Group.__init__(self, possize) self.title = W.TextBox((0, 2, 0, 16), "Bio") self.btnStats = W.Button((-40, 0, 40, 16), "Stats", self.statsCallback) self.btnLinks = W.Button((-90, 0, 40, 16), "Links", self.linksCallback) self.btnBio = W.Button((-140, 0, 40, 16), "Bio", self.bioCallback) # Bio editor = W.EditText((0, 24, -15, 0), "", readonly=1) self._bary = W.Scrollbar((-16, 24, 0, 0), editor.vscroll, max=32767) self.bioText = editor # Stats & Links self.list = TwoLineListWithFlags((0, 24, 0, 0), [], callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) self.linksData = [] self.statsData = [] self.toots = 0 self.following = 0 self.followers = 0 self.locked = "No" self.bot = "No" self.group = "No" self.discoverable = "No" self.noindex = "No" self.moved = "No" self.suspended = "No" self.limited = "No" # hide stats/links by default self.list.show(0) self.bioText.select(0) def statsCallback(self): """ Shows the stats pane and hides bio/links """ self.title.set("Stats") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.statsData) def linksCallback(self): """ Shows the links pane and hides bio/stats """ self.title.set("Links") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.linksData) def bioCallback(self): """ Shows the bio pane and hides stats/links """ self.title.set("Bio") self.list.show(0) self.list.enable(0) self.list.select(0) self.bioText.show(1) self.bioText.enable(1) self.bioText._selectable = 1 self._bary.show(1) self._bary.draw() self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() def setBio(self, value): """ Updates the content of the bio text """ self.bioText.set(value) def collectStatsData(self): """ Updates the content of the stats list """ self.statsData = [ "Toots\r%s" % self.toots, "Following\r%s" % self.following, "Followers\r%s" % self.followers, "Locked\r%s" % self.locked, "Bot\r%s" % self.bot, "Group\r%s" % self.group, "Discoverable\r%s" % self.discoverable, "NoIndex\r%s" % self.noindex, "Moved\r%s" % self.moved, "Suspended\r%s" % self.suspended, "Limited\r%s" % self.limited ] self.list.set(self.statsData) def setLinks(self, linksData): """ Updates the content of the links list """ self.linksData = linksData self.list.set(self.linksData) def setToots(self, num): """ Updates the user's toot count """ self.toots = num self.collectStatsData() def setFollowers(self, num): """ Updates the user's followers count """ self.followers = num self.collectStatsData() def setFollowing(self, num): """ Updates the user's following count """ self.following = num self.collectStatsData() def setLocked(self, flag): if flag: self.locked = "Yes" else: self.locked = "No" self.collectStatsData() def setBot(self, flag): if flag: self.bot = "Yes" else: self.bot = "No" self.collectStatsData() def setDiscoverable(self, flag): if flag: self.discoverable = "Yes" else: self.discoverable = "No" self.collectStatsData() def setNoIndex(self, flag): if flag: self.noindex = "Yes" else: self.noindex = "No" self.collectStatsData() def setMoved(self, flag): if flag: self.moved = "Yes" else: self.moved = "No" self.collectStatsData() def setSuspended(self, flag): if flag: self.suspended = "Yes" else: self.suspended = "No" self.collectStatsData() def setLimited(self, flag): if flag: self.limited = "Yes" else: self.limited = "No" self.collectStatsData() class TitledEditText(W.Group): """ A text edit field with a title and optional scrollbars attached to it. Shamelessly stolen from MacPython's PyEdit. Modified to also allow setting the title, and add scrollbars. """ def __init__(self, possize, title, text="", readonly=0, vscroll=0, hscroll=0): W.Group.__init__(self, possize) self.title = W.TextBox((0, 0, 0, 16), title) if vscroll and hscroll: editor = W.EditText((0, 16, -15, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, -15, 16), editor.hscroll, max=32767) self._bary = W.Scrollbar((-16, 16,0, -15), editor.vscroll, max=32767) elif vscroll: editor = W.EditText((0, 16, -15, 0), text, readonly=readonly) self._bary = W.Scrollbar((-16, 16, 0, 0), editor.vscroll, max=32767) elif hscroll: editor = W.EditText((0, 16, 0, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, 0, 16), editor.hscroll, max=32767) else: editor = W.EditText((0, 16, 0, 0), text, readonly=readonly) self.edit = editor def setTitle(self, value): self.title.set(value) def set(self, value): self.edit.set(value) def get(self): return self.edit.get() class TokenURLopener(urllib.FancyURLopener): """ Extends urllib.FancyURLopener to add the Authorization header with a bearer token. """ def __init__(self, token, *args): apply(urllib.FancyURLopener.__init__, (self,) + args) self.addheaders.append(("Authorization", "Bearer %s" % token)) class TwoLineListWithFlags(List): """ Modification of MacPython's TwoLineList to support flags. """ LDEF_ID = 468 def createlist(self): import List self._calcbounds() self.SetPort() rect = self._bounds rect = rect[0]+1, rect[1]+1, rect[2]-16, rect[3]-1 self._list = List.LNew(rect, (0, 0, 1, 0), (0, 28), self.LDEF_ID, self._parentwindow.wid, 0, 1, 0, 1) self._list.selFlags = self._flags self.set(self.items) class TimelineList(W.Group): """ A TwoLineListWithFlags that also has a title attached to it. Based on TitledEditText. """ def __init__(self, possize, title, items = None, picker = 1, pickerItems = None, btnCallback = None, callback = None, pickerCallback = None, flags = 0, cols = 1, typingcasesens=0): W.Group.__init__(self, possize) if picker: self.listpicker = W.PopupWidget((-68, 0, 16, 16), pickerItems, pickerCallback) self.title = W.TextBox((0, 2, -68, 16), title) self.btn = W.Button((-50, 0, 0, 16), "Refresh", btnCallback) self.list = TwoLineListWithFlags((0, 24, 0, 0), items, callback, flags, cols, typingcasesens) def setListPicker(self, value): self.listpicker.set(value) def setTitle(self, value): self.title.set(value) def set(self, items): self.list.set(items) def get(self): return self.list.items def getselection(self): return self.list.getselection() def setselection(self, selection): return self.list.setselection(selection) \ No newline at end of file diff --git a/third_party/PixMapWrapper.py b/third_party/PixMapWrapper.py old mode 100755 new mode 100644 diff --git a/third_party/__init__.py b/third_party/__init__.py old mode 100755 new mode 100644