From 5aed3e6738ed5c16ecce29a4b334123d9fbbdf9a Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 8 Mar 2025 21:41:30 +0100 Subject: [PATCH 01/92] version set to 3.3.0 --- CHANGELOG.md | 4 ++++ manifest.json | 2 +- options/mzta-release-notes.html | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f07e1223..445691e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ +

Version 3.3.0 - ??/??/2025

+

Version 3.2.3 - 06/03/2025

Version 3.2.3 - 06/03/2025

From 75502e556da233c43b17429e66ce06613f09e378 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 9 Mar 2025 22:02:03 +0100 Subject: [PATCH 10/92] Update survey feedback message for clarity --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6caa834b..1fdc0e5b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -884,7 +884,7 @@ "description": "" }, "prefs_SurveyLinkText": { - "message": "Share your feedback and help us improve ThunderAI!", + "message": "Share your feedback and help improve ThunderAI!", "description": "" }, "prefs_SurveyLinkText2": { From e4f31feaea97b4c309ba32583184ac0960ffc128 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 9 Mar 2025 22:16:49 +0100 Subject: [PATCH 11/92] Refactor styles for #mzta_top_msg to improve readability and aesthetics. fixes #283 --- options/mzta-options.css | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/options/mzta-options.css b/options/mzta-options.css index 73c9ec4c..0727c930 100644 --- a/options/mzta-options.css +++ b/options/mzta-options.css @@ -191,20 +191,23 @@ input.option-input[type="text"]{ } #mzta_top_msg div{ - color: white; + color: #212529; font-style: italic; - background-color: rgb(3, 0, 180); + background-color: #e0e0e0; border-radius: 4px; - padding: 3px; + padding: 15px; font-weight: bold; margin-bottom: 5px; text-align: center; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); } #mzta_top_msg div a{ - background-color: white; - padding:0px 3px; - border-radius: 4px; + color: #007BFF; +} + +#mzta_top_msg div a:hover{ + color: #FF6600; } .conntype_chatgpt_web_option{ @@ -251,7 +254,16 @@ input.option-input[type="text"]{ } #mzta_top_msg div{ - background-color: rgb(2, 0, 100); + color:#EEEEEE; + background-color: #313131; + } + + #mzta_top_msg div a{ + color: #00ADB5; + } + + #mzta_top_msg div a:hover{ + color: #FF5722; } #no_sparks td{ From 87d22ff9b1d35bf9886ddd28321f9f35dd172c30 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 10 Mar 2025 22:39:04 +0100 Subject: [PATCH 12/92] Add onboarding page. fixes #256 --- mzta-background.js | 11 ++ pages/onboarding/images/autospamfilter.png | Bin 0 -> 72923 bytes pages/onboarding/images/customprompts.png | Bin 0 -> 41080 bytes pages/onboarding/images/prompts-button.png | Bin 0 -> 14542 bytes pages/onboarding/onboarding.css | 183 +++++++++++++++++++++ pages/onboarding/onboarding.html | 86 ++++++++++ 6 files changed, 280 insertions(+) create mode 100644 pages/onboarding/images/autospamfilter.png create mode 100644 pages/onboarding/images/customprompts.png create mode 100644 pages/onboarding/images/prompts-button.png create mode 100644 pages/onboarding/onboarding.css create mode 100644 pages/onboarding/onboarding.html diff --git a/mzta-background.js b/mzta-background.js index 0f8b5420..a1634c16 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -26,6 +26,17 @@ import { mzta_specialCommand } from './js/mzta-special-commands.js'; import { getSpamFilterPrompt } from './js/mzta-prompts.js'; import { taSpamReport } from './js/mzta-spamreport.js'; +browser.runtime.onInstalled.addListener(({ reason, previousVersion }) => { + console.log(">>>>>>>>>>> onInstalled: " + JSON.stringify(reason) + ", previousVersion: " + previousVersion); + if (reason === "install" + || (reason === "update" && (previousVersion.startsWith("2.") || previousVersion.startsWith("1."))) + || (reason === "update" && ((previousVersion.startsWith("3.") && parseInt(previousVersion.split(".")[1]) <= 2))) + //|| (reason === "update") // only for testing + ) { + browser.tabs.create({ url: "/pages/onboarding/onboarding.html" }); + } +}); + await migrateCustomPromptsStorage(); await migrateDefaultPromptsPropStorage(); diff --git a/pages/onboarding/images/autospamfilter.png b/pages/onboarding/images/autospamfilter.png new file mode 100644 index 0000000000000000000000000000000000000000..b82f4553251465b848070be2b75d18db438abb35 GIT binary patch literal 72923 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

4?6C9na z(5>+Zo2hP~q|wusTzZg+UN5Yy;+H)*v3bFQg^O2if0Dwk+I;u2w`mRG3Y3 z^Kk{^50E6m)ytidSBzRYs?0`p^3+;>5qq4ww5yxFO0KrGp+gi3Og6K&z^i2<1p;dz zW#QGGvu4g-ux#0?!*^&VB(0^H3f*T9YfWekqEe{}iUM678vSRHtOGp}QB@Uqdkux* zjMkqZIvJP}sN70SZO5Et7A& z_ZN#e2#m3Fur@^phUl(%9IS#6p17#YwUd)Mn)J7N!M)cYS<0K(N~PAI$kM{x6g6_N z4;(dS^oCsrkDoYs{NlsM$JU0?G%6UQ-8qil1-cOoYHwg`Y2iI~&iYd)jvYO5_WJ9j zeFK|YU?6l)xGqar0+CF@V-(W#wX<_a{Bq{x@xxmejP@s@o9fX&LkLzNRBMOqD0ow< zwf~fBXe*#`|Xx%NBF$Yf`KeA`tyg{B{80mcv7KjI$Zwy2r zlW-8pjArdOWBRfq$B%+kE&s>`8+`++#6`I^1b0`^_RuM7<4d87Wlc5WJYfFG(>H!QHalnRnn&-L zkg2=9xzEUjTTh%ge)PooTW{kxcXp#1Xm|kB+yphrN2^l{20=}+(O=pnIRis#NY%g- zYIQ9}&gD}{Safxw5h961^cg;IbO`=oat-X$(mJ77=g2|M4}X|Vb@g_Jfwn=wq%5e5 zd;Z}kLaP3ig+@?;K+zpS7%x8wvy27O#z2U&BEL-H>!Hmf77OVfS!M@zQk3`Libu?cXqG1V$I27y}ViptJhnrw#%E@8gxvs%`voBd*UX z#Lyiaz@tCc3*d8kX#*?caGwem!jZcTc45Cd6IG-R3}o0ftp&geSwmfQolt9YVGsnu z;|bbw;+B56MCnHm?6n;c=!+Fn>E~#@hCwZUu;3*>zaU!P+Ejp^)(Q@WfF}u}l6eYLReq}%2<{_R z4e`2raO1dGV~IUyyN+kmP>jf}$+e^rY(ET&Csd2Vq@ZuWy?(a)|#fI%#J-pZNL ziXQdo-Rs1(^t=+#)=c~YM>s{^jERU&OG`;i&SX{_{6rVv168mqbFw3@#s0Q6Ty4p~tosCKhPFj8XB6 zGgGs(Gj3f^F$o*rg-$o|?KjXQ_ui?uIk{Qqm)>(8HNKrCq*g1~e4X_XqTtp`RN(#i z?2@#^q?FXOwB%av*Rg(8XmE?=eK=BH=Gr=+EXrzIvN zWEY~Y7GRB>CHRyIhbuLt3}yLmu0Bgjihubg{24N$o2Lb6hobWIlw^=>T1IC0%~*?m zt9tvGSo9s|SQ~lm@;mT?r{~;jw{SwYW*V8GR;WfRy5-QmyafxhBwqzE{su%NW{E$4 zLL|8NoDf=j^FVlRUiOtu58*bGd%EI4cOt8;(|M<81dJf~EIAsbS5hxTB;FOGOZkc2idOGux6znpWlkk%uG8q z{ZY4R1B1w*@0GB`x`Z{-Dxq8h8bEDnUTOj;+860*iD{XId?6~dSOTH*sOB5DEyc7B zcD4SrL@3Tp*<#1AJIAasI9x#oyrl^Vi3yo$AKstc8b5k#-ykbX&o1NJ-rn>Oh*S8j z*RS5Xk8kg&&v6jAUCm=jRE=3wOKYlxDvXJ9`{B0nZ^IwOq^G5vn)+(sq+Wj7@UiY~ zT3Cjkdz_eRP@?)F4ur zTG-iWgZ6)irN6H)QO>NZM_m1T%$(WY&B2bYcbOWCvuNhSaCM@8;kl4vX&(@5U8Q}$ z29eU-(hiJet4j(is_KOzff%B<8`Q_kj*dYZ67N4@n)}-0d02MNQGerhF}<) zi-)_XldZYF**3|^&7vx|oTtPE4IkOY%^titBB{*CV}bV-3ixW9W`jrfv;*BG(WP}O z2Wf2$r$J!fvER_Xp%gHYF!S?dxH*{6a8&!C&fVIDg?KTnb<6nQfgxBobMbVwN9UL5 zA;!d~g*Q=HQ^V!ayjqSN+rtJ-aWPmEA3uhhizyn@x$!zI$vK$e>}q3a=i}q%>}cDp zx0{>0zo&<KdLv=-PW2nA4NMXq05x!rjruo(Ar~nff}rc{*B=>dFessyG&HCpLE@Rr55C3_ChH zD8g#+s_M5Uy~hsqu=!M|7y{9;ch}J^{XK0+L<9Xa4h;1jq-t>kUxD~fn$pkDk%}NV zJ9qB@D-o-Pqb8X3ojM}W?mNexS}+)bOEVXDH#;*>f_O8J=3dUupb^;x1)FoLYI!2D z&(P8RI`}shFUi)`C&*4zQ^yr)EUoP<(R;o}Qks!dtHuWm>1Re#a3v%!S6e)uFkAHfJ{T=)_%FJLINbZew=W9Csvb4Ybx1;KRK7aO&|LG!bXr>OPCqJIp8pNm+ z{y6`QGSinv-TNCG=g&TrWIBF9?|-3E02u8l{}8DE76_+xfMVpI3#?KqQD-_tFhluw z>T{!j{~<8=Mz2Ul{?VYH%_tPozf~y!j52P-h>>skRxiNny4Z;5JVWsz{#&SuVj^R5 z{{IT)X~f9ChHv$PMqPJr>-CIU-QoWgn!LOF@1_3xg1{Ov^8W?j>IDphHE}X0g42jf zrT!{vibkyhH2E6V5Au1l8@g}plOG7wbFr2$eP1-5{N{n+oLa3>VQF?IT01f=;7J;E z7n~6zM*bUox4wYEk|~^bA6{;nws7h20ejw8Hg?(-i63@P8nR^N%CX}Q=7|lMC_tj9 zi))wNsYkbhiK}9E%=#^*OannmVb=bM(^jusxopAIE%!^caho7+X~Bsp{g?i-a#){D zF>IYNRB24)uak!@Ua?}up%-7+0dQ`w8`EXtnQGmk5hF(aUcS={5Js8)xK>7)dhE=J zvnz!A7r&9Hg;nVXcD@?4|MAHqM}8g7-?t~+fUP0~s}+jIO|W3Npp-UlH!eB9F+FI~ z!6QeGpZ#seXfHDoq>@*iU$)J4<((6Uj$GK{eQ4z^0R-_Y6L)Wk7_#fm$s>nPtR9T{ zWWWOvU0M&A*vDJ%W-cQ}jQr(%rxy?n>^FPjz^)!95M=LU=SSl*WfIQw7z!ahD?aM& zTc*7JQu3R4LyF;ZCl7w;e%p4L-?H-T!MBlbKV*uesM=6-Uq#3$70j%tsCNZ*70lRwJ(&Tx?8(yjS!nAAX-d2jF>uiN>5AOp%EiS{$9QxFQA=%Q)EjF22rBB`!HAn zfk+}#C}bkVXJ*RY`F{Er89;M}vDz4!X#68>5Gl~Uty=b;wQ@s88d8w-;ONQM61f_S zMs&h!zY*gx_I*ygKN*IBRBR62me@Eb;Pdo{3N~WI$bSJndI5oTQ~QdTwAkuS!`$dJ z>w)bXggze4!rFK2(5_8rke|I?+a<`6f1(pBgb@^JfS4zbxcn2^c|@>G&;kdk4GYA*jCm z-Ql?hxn3?U@-Q7EG^E_+<#&xf|U)6cTsv&69eu#5-^Doz`+MhW$ z(?Oev5hF(aR`l9OfO7yai}=cSp6)C3cBS!uY(6_#THfn9e-LD&`?ksx#$E_Az_SGd#_kH`sPY?X^JbLcOk2hk($lr`! zy?E_`o{|tl9M#6TqZDXX)@3<^nEJbBFC*9~f;axh}# zU&asO)}rwrz}Nw+Uv(HUV&oso4;noNi!~|*BS!v>7~8!OBSyXjw`I!~KA+Fy@x8n} zv}ZJ;lJFbR0voI*|Ko#fqPK?y@&E9@Zq44SDR}w2bdkgyiziO2ef9=N8*tpoZVzrAC!-Qh&0F3+r zz84on#(p&Yau%nuF5~?4Csp#lW7#qK+<|M)a&-p~lzQv*$s5VKgRdc2IkoOLs=46Z z)mNpuQ-1)Bxcusj+r5kF3rcuW>tXi{b@zMja<#mnaD{tW3-@a)1>R(o_yI3gsL3WJ%6neT5 zEXiYB2P?B?=GL9Yxsh}iAh22{k%+|-fLzY%t&7io{8*&D9)hyNBW~2#HbZ~H8acRGERo5TjjawxHMMnc zba!#J`7GRsTB%T|wDJJ51%5U~zE#Vm=*?33r_A6e0TaT^=mFM@ZW9=v$32KzrIgEc z5%YczVQ%t~HJ4&)wQmiQAegp%%xxIo%9Eh|GlFQ8+E)Wu zKrE9=S#qKV!+Kx6#^^Jgrss^Wcq7|JC&^2TMn<-?Dm1m+rCDprt0Mi959BdR+dWRY?D8e(*iwOJv#a?p{9jp5$kzf=I1KZ5|z>%KLC__seVn z&)OSb&2Au1*}b!mGeINfKRnHZcyV~mbBCSB##mqle_c2#%*Svq zDUQ0dwfl5Qw(LkVa%1Z@fUGX-_0~3z+r@1h9^#J1(kXp(@YI`}C-2CYu5WA~Vh_$p z_{lpLZ7T}t<;<)S6~y~)d$6JzCib^Iry>fHijltEeQuzJl_Vv>!6;UVmzb&s3 z$_O;C4l9-ox5Z=Rf}Epsw>5aUa#`tTuNJMj`(fiyXJvKCjWx@dp23Ch#gmRM8`|6z z7S=sKa=1X|PDEmh>T3nfHlJM7mj2zW2$qRC{02Gm<(j?8qYkZa4MPM=OKVd)>(<82 zS#5`DF1(aci)Eb;Y@FQOnaD1TI=tu>G0cJUIzQy>?wNtkoZE{B-L-F5633p@-IQ5d zQXu=~+^IoMx`%&3*`58X^1Ghh)XEZ)mW7`g`78P8o#`FyzcaL}e176+curz?5~EWW zZ#yh(*LUlJ?s%Ly`SpXVr()=V)^%|;Lv9`%Xk(^tt}01<^g_|OSJ1nS;|p3ITRYVg zoKt*UJL`1I{ntm@LU7H;LmN6(O{m@M|9une>$6&hSQ#f$e+Zx#ii=BLzKj4r456wmJ=MJdbg{o#GS8vVPZ{z}hg7n3VA*}qL6GUG-f|N1~^=kquBF7KXptWYLVgwO4~ z??E!Su<-8A@f$wEAQfI!N=yz{dw21fIU|={`;=b!%?&d)zSJEc4KI$&UwE4bI##5i zEaI2(*V8IAi0IOcK}X)!fOE?7*rfwk1CBto0tIdN3rc^Z^0llyU*KX^x0ulHfd2qslp(!mx3{t znEPxFjia~yuH`g|^ z?|Uh_2tjI|?YA8Is6@z5Uf65K!y+_)%8Kx1eWyp)2&yg(?z!*v+ZWMGeL6oX%-B0& z&Y6!OKi^Ridurs^CzUz5>9Nl*%?TMjy5E)$l?}Q-euGL-w{ODgsJu_lQP<>L?>pmo zE+0LpkJ~?D^1(ut_Adr#xHG5U&P&CO2MOn9j9Qx{D$joZ;PkOY?K*dyb*-9vYweWn zXR`zqnJ-VQ*}bY;%bveR>YgaC%znJ0`?C8<6$Y|rL@z(3MRDf#otMmweR}NNqbILp zbn|>fBej|`e^?6&m@3$HZ(mnlUoC>@{@$jXSNHBeeERTy8Oe`?$3h51u?_7IprU)u zY3tzNZlcPsti}_ekjZWDX0a-yb(#6O!J~q}=n`*X<;D<1KfZhK{)1E|mu5ox+4W8g z4wA*k*NR!?2@fAWj)?tGo3igg%@15tlDu2>oHVts1K|e~Z)NheUqTizCrupC%vLw} zZA7C|so|!WyyyU5jhQgEd$123f~Y1`qJmqmJ+D@)VOXtJHRS?kGHc#*<>j{@JbC)y zPAT`+*(OPUdwZFYSb5|0>tIw20i)IfA32ZZF-n<3N=M4<-1mheq9cVRW_V|uo5M&ZO zG<;*IEiU&RFC?s`7a3F#!6m3w2la}eI~r6h!n>1;#5ssz#ouI|xag2CX- zl$n=S?1`zPhs-^`a=f!lH}Cr%P+A%oomlCPH416!+n_$p9nH}rtFT_a%Cd|ftsQ+m zlfhykLFtF{n=cg7S`S&YZbpA1n2`|)4T*0aUp@1Z>pyA~+(&Yhvrftb*IGi)(#HxD!A= z#5JjhHr{N&xiky#_hbO6|5_kEgX%L(OIp!LEq z*_@6vDl+@fwyoWP9x{Yd1ys`4OAw$A=|uDg)!DOEs56~F!D}>1)M6IRph~5I$z;&p zVbF*{WueLs2Co3}uTMhgWPA0KUY==0^`XH-)2EUy(f2-zDfc899r(2(ex+)035$b zL_t*Ojz}1S6^h8Sv7OukoS~YuypPr$`nPP}%+|Hx*~J<(a+FpdfR>A>!Jr+;fkvi5 z1J#*O857#S>oQ9!_(!8rDb?t_LE8~sS9#tw6 zgZ2fZ=bSv9`l@oc{=NZ0K|z7dfbO#*Lgm@jiY;YlMva=hVcnWh{hI*=iJ^7t>8R2O z2?6N41o-*+Fz6Um8LFDrB!3rh?WZg?0`iD9_s?Mcdw7*5M#rJSfJ@$ed=Y8WE0_SX zue}Y?4s+0@PTk9VgGES4*pXgoDM~CXt*d=}x@<{1cSd6*^6wxL@E-W&qPhwN`b3~= zNc7Nt?E6VY-5CHF~W z@RAm$Cc!h0cvpzI72ssS%jiU8;}e&PM&VQUIbn?8{4A2kEpPS;@_O6sIO7D^l9UWCu39=NZBdM zh~vY8txa$c&XnlbUV3xu<>wzG63lvbv&1;GZ{xvAezI@fuJCe+rXlD4>r7KCcTZCVO}_dpO-K2YSY5W z41zetnYHBBU7MRg5ZT_=gjRm}`1yOU;=~xI=Ds%8o^DokZ;ze0^y2mN>(6o=96ene zoGDc?r>=x&r=%ptKRbN4AS58b&dSuglV8HYombwxef8|YqZCZfc0L3|$;>aL__lRt zfM@?6QdU-p%)fhkhVB>=m*nq$G=J z5)@CDMoc>xUinxDp|GuY1FojnceJfJz z@9k}88_=mm^_^pvpTD@1jN9<*9A6hR41wy?-9Prgt{YKrURMX6**V67Mt~$W+4ryF z7w(-L>ihKzSW4pV&)AaEch0zP{$S)Ypdb3(bw_)G7?+y3qNmb%3rXz_}`0yU65YFmTDka1DSb+G?FR z#&0)9j2QWr@AU$rQ3&}0sZy?#2>7C(v?2H*N+GwdzK+8ad@>XGn}9V60k1*wJH`Y0 zfRz&Tr8OFtoAE*9{`%0HYqf~bUizDTP0i90ni5w{M7(;)uq z3lq_(#o+NA4k$(Kx7EK7L?ILN`9cF9LjOWk5&;L4S8Y942xe>lRy1lkXc|)G&w5|` zUN304N$V}=+{=y5zCF`&Qwsk-FH27H%PS`j40WF#naI|i`j6qWE17lQZeSDE$F{e! zIR7#K4;xO3voEf-oN|}naDTSts&g44-RYkKkygH)X}S7cMRd%;5tb(kP&*yDgjZgW zmz$HFot;}+S^wiQRLS$^Sz2zq6(iN1Fa#p2dUtr+zWv9pT)cKWI#-}O{nvrWMS0;@ z53ikRHRv2)^w)$5fK`gBawcE=O&Q2jpY64@9G4;bTX!MCqSB|E)*Lx;`qKI9k5fw; zH=+CsLAcCFb1REm>199Job}8j5RDQd!vs45Ooo(yD4Ml_YZ=`qu3EEx^>nJ{Kld=* zw6Q1k)Gs?j{#b%;ANrogw;@v8SH6v!(#iMtRvhc%BF`6w&Dgem z)tVIx2DJg%{Rw_@E0h*2&7$@Am7A9?SmE}g;ldv)9+~jmj9sbBZv>L!+ivW;h)vEE zgI{0%P$~wfeD&}mX4Imsn^v!1Hoa?*sqXZ@4vdvUXzZ(Z9o=kxa^XPFz9u}~e@q)c zHxGNK7UNpEFs!I}(3+)n?ALd$yh%vUFPFOpxRBKih4ByXreFiyZ3#Fe<=&M9A(r86 z#f*Cs&k^Roc@-5KU#6lvFl-tfg1|Ci?wfFIvmU-KX4*s`wN&)+_Sxrg=@}Vi#2{Z= z99CGL_dYQzzqly&b$Ch<3-97@r`+K#s@vNp7nMH0dMhd=H9ft8(%jDmi)pCIexIC^ zUyvUY5s_9>O>ptG1=r#zjAj!$1~}Td1dnbR=x%3D(W(|$T^o7xX{mzj=xAm5s|%K7 z-fDcPr;9tov;Bl%CwmJrMpT!RV%m1;!hT^Pt?f%rL{ta#XzMI3OOMKukeIReU%iPh z5Lj3_S()OvDdBft#MDDHM_U&ME7W8YMWar9aN+TX#MF#xD<4-2qBe0vQt>_#Y1gy8 zwfT>0mz5ZK|Jlp9q#P06+{wWLC(M2Jn6t$e85hdZnbgG%ckBX&&GRNq8xb zFTX8rfT@ll?zTAHrY22A#`{MPU&be=mIyR<9!@k!UJ(B_p)*cxy{TZoM_T?d;#C>N%heQ*frVvB zPa|tgT7|z?@ZiL4tB`4&mJzN=7vPM?<=0&Q~(%psz6i(vv_a#=ouB3+Qr1(Nf zH8bJ;vxuZ(Imy)(l_xzkWPgY%#F0y$-+UFF%EY)j+EYmw&>^Kfy!hySLP{Fj+{?vM zYuyVb!|!jOeU*@rmCXb~YmN4%aCLFgliQDDQ&Us2N|fe-&KBT8WkKw#$Il`YlG0cj zf`b!iF5=9n*U18D&hsbl6OvdsYlfXU34>Hd{&w$Uk(g-d=j>!kLlv=HRQCRTLLQr) z@Zx!5PKm}T(2)vK(Nq;AJi7ZdJ}vY6V#boKy@R|Q=_I<7SCF?AYP=-p*InO!TxHXG z*w8_m$O{+lWkPs!7blzg7aOl+^Q7g^wx5bp+jyBl^0Jg?1BXm}gYDkCO+d-sZ66A1 z^iS1QvdqVKE9{4j7&_RlK4sUA8?_KD7gZkZ-s5BiwngtDE(Of)D96j4~9}wo}>K@docb~q4dbYGONA-tAvrgSw1u#z?&aKo9u$8i+ zbK5W0n711`WN?r?;mH2G9H5{CW&68zzfetS-FvV@=7oo^^3Y2O7OuWQ9_B=%nR}40 zn?+N{4a2-g6URSEE&tJw8iS|X`vtn$l1b+7zCrG0Kq5g`he3@d(A#({k-bZ3DEOtU zExxmS;__b;{Cjlr7M$DhA{k6D%sfN7cJ82lusu4vPWw1;HxhMf@0(Ja5hF%;!e#po zJeSE7`FD1&UGwYNCrOX@?_IZXwE5NpE9vdtgDLI1_wODS7_seeN&yctb#LKV zwe#r3bdHo2cW@ia%!X=?hxrjRhYmOqjSC*#!K~)|ub0a|`_T9b1vBgA2aa3&5d->k zuh_r*MnskdLfDzF2X>p4<q02 z?AFzn;oho!_db34_H5^Y#XuM`&8zd^*3|kl``$=&3s@Rv&Wo)_i~QU5>EF96a&ObE zs5}@#DuFnST3X=Vy;p1h!V4FROB=Koe~VgKc5%nqa%x*p(aqtk6Z`M5Wa6|dTUM{# zdp#oQ_Kxjq)~?@k=~0>P@~@$i*oimyJSqV{VSatond<~KmWFM3a_rEHIzR9(9T>_x z+fL<46lzWV!y^&8EYzf4KKWh<>PjliIMJ#@`^#`ZyKYXI>(1QCF)#_06uw{HZ^4mg z-1fagNom`U$D#YcW6vLWnrjX!+Lus$V9#T*0)`aAlqb))ZX<^e8Dv`Ydhh=GU{tKB zFL`$2wv})DK?D2u>CwRl^;@PajK90(N<-Uj{rYxk%{{yMX=(+yu;BIifvXSI1r6*O z=n!-AW_1min_{e+ckdeHntR|(Nwv5!Gb%;R^*;T!rEqRp1&CnP6{gXPj9~M z7}#+@KffP$JdLRN+{Z6)o*wO5+uPUkMAsgpLLBTI!sd;(%X;>K4dXBxyO}q)_3s+!>C~yMPkCX! zep%CkC;IG{O7~6c_5&Lwy()z*vpo|U<@DTRZ&u}g0OfzcJi*< zOS^h_`1^Ert}Wo!tHFe^)38|soULu`TQ_%WtE%Ej6l8Nd4|jKKQ%eRIO*y%`Iaz^j z27@P=+c|odM z#>%Es?`HOeH(v@R5XrRF@S*+M*jn1y_v_^+O{$OskpT1g^xulCsiqE&E>2F?Kpf?) z?C_^;7k3Wub8~MN*nMHU@HYk3FrI8`xqQ~anQa(e&HTd%xrHJ$1`O4j;pOS+CSVNF~_27|Yo{fVkffrNnzCt@b1iA`c`1p1Pidr;% zaSpC5x3ubs&ceY-tb>yUR80FOeE3iCS-~M8zpoMDrsOne0iFZM8hMnS_)JsS)~F4B7)D5N$zu3x?h;= z?}5G%0{(|5k+oF?z~S*4L$V+)esFScc(H|xEmfLQMjwwuvx*=DDh>631;ZFTPNU7e zsNnY0bsv3#ov9Mmhji*@?F2!sQL9nYvF|GjMnGTCh)k|z$vr#M8V71*6Hhv)LL>y5 z6d@3_`#&`>76($%zgPk@D@?80T7zHU&lr1#C5}}E;l05uh)yLDlynnQDpmKM`oOkm z=V!L#T_(RxaYAtKvF>1qo?NXqadrX)fzIjQUm+LS_4__JwUj1c}G;XVa?1f z(ZZwlJ$#_ib&!7OUV+|ZP$s4=d+%roj%)6fR^=3**^@<-qZREM05{+uOJQF4wiTDf zt%K~y)$ttSU~nP9(Qo3pXLo)(y5^EpL>PE{>rhjPL|#MmGNov9fuosu!c_{X6o@oL zq-b9jhOu}os2B7NnwmyaaRU$x1(c2M0E>gw5Qa|kF}t*{aJ&cBO{nXmeh87sXg9t~ z?^T<&?b@?<&%XUTSI%sMnn5cIZk$f`AH8l7f9uwl0_`^dbZ0f5fEJljQSv6YrMp?9 z$0aP0gva3s1l=N&T$ZwYhIqyPGrNA>x_Dx5Cj>~2mQVGLz5|X3$&(Tu?U2qpdHUe5 zdsmL_VDic77ZoEG3Bh>cD{v&@7pY)y&05uF7gQ?Hf|1r`CFJ1)y)g*-1dvVbB~7Ur z0)fS4xw&Ok+&}(F5CKh*gpLxhHXe2?K3}4(9&OU!rAj0ba5#KZ;XwAGEg1o0=4}L} zy?ghbz592L@YL!i^j6TE^|uWjBnNAokio;}?BBg>_ntlbj~!pxzl9B`egX6K;nP7+ z_qj1I-FpEBoIvXW(Ey2}H>pc2Qqvs5oONx7NFd;_cp|AWKeDW>XOj!YA3Jk+=k7HV zI@^8zh>K>X@dsL2Sg7*!zWdGQp4hW%$EIHF zn9}_1KNi$AUc}CMD|RunwFE;aaP;|o!4yRI>uYcUEgQ1)%fPrC1IanLiJ5S7U#-}o zb|-pE{2P4wcXfMp`bB&ZJvhV%&5&1sNmW)R7z2R>WvqmB# z{dvCd6DjCuDzQfFLz>(AjhM4RSJ4M{kMX981hMm%(>5PD4VuRM(Ot|mx-W}VuU1nx zA3t&E#9+bx&1X{}YdcdfTyzmSeFLq&JTsE+OShnFOYDoxd@3(IQQI_#O)`d}%MADr z+6jZ1j=bJ(v0H9gT10rEp+4eEFxGZm#)a&^yZ3ld8&{!ITPX$Ydvpq> z5o~8nta}z7jx8&Q$@y@*ti^6r!6~cj6^-W0h)Pf;l&c`3yPfNBY5a}bJTpS%ule|4 zA-cT@&4v$G&fI&}af}6G*|B{q2OCXlT{w!?O!Z0%|m!+uU;u?D|M9Hre{b<*KMqRPKSs?b%^HWRv|AZOiXTbAI_dk5l zOyO=SFM2GUzq|(n^t)DSeyHeTh{H=x^2WM zt-)9mrg#qQ9z;YvRKO|;ThydN4Per;$M|K}8zfEW5zJGG9pjl4`SD)5B z!knF~Tvij4Z{E>3(%v7MHLC}GMQNs_pY!)ds(}Ax9-};HL%|pyIqg2 zHk8|#B5%*h=AP}$fK-`cULB^fz+jbtDf`+~{1ar?9t($Lo?ml;)88Tf+{>VaOFP>) z>Nt%;SSdxrBmEBkefyBdbj-aNF~&*Ta?D%JS{uK0$r=1)_q5t2AWyfjxq$ zB>VY8D<3_7Epg9^OiO-V*YOC12Yv>Qt(0;~3Yl8x#u$p3v%fQ?uA-=p?CsB>f}TQCTaeAC zd3akUU*3F@)o1k}Yb=iB;^t{>21;30kyp%9XwcqMt&k(8fxfO}5>8WBlwB>-sPPsI zb6AdNxI18_999i%>u6^}f;60patYbl)sCnT)iBGdgc>}7VrEOnK{N+D3vHb%Sp`KL zsX~pRd3m^*o5JNeMSK+kdO1X`P$1@kel8>;R->*d$gY;d2%c=|9pDDKUAef9U5~MI zw4oCrb$xk-jOyZI+cci~76g`57P25S8%GE2K=EfmWMVG6yh@@5V`VU5WcYg8fjp}k z>X=-Wv#Z0Wr3BPGqPV0^q#%&U7+5J&Q+>T%P0$r%X?adDPl*l#5rTEFzY`vmGA!p- zv8AT=4AUPwc!s&GGG@IHv|bHFc5(Bt#q;u+e2POeS1>|W$+*mdDx9~EL(Rdl8<4SU z2m7kjMB89LXV7IR<(!h-3OUFP9;;9&iF8JQmo20M6;o2#pu%FwmJX(nn(D+b7gSgB zAbWQQGFGE#U^68q_6`xvVgk<_xP)i%xybm{*`AW-F=VH<6ipPt_7)3lLaoLwnY zX)suto4=O@5fnM9oAN98N;S#R%S6h@I=C{(a&Z;Ao^I!8*68)dF!FjPTTJqFx78i% zkX7UsaTVH%##;sYyAW|0Wow^y?z3dLx!IY4lCG^}$|!DbHaJKL1icorb+Vyp%CZUtFqTLpz$%3bYYD^& z(g>JLsX4>Z0=zDtRnmZSb+=$go!=Kj9^5<3o}@y@K!3{4S}%g|lM83xKphhr`Trb^ zvS91TMYmDUl7E~=yYPPoF#0m(k9JO7danFO?mqwNNNN*zK&>vtbF`KFspI-fz*5Z^ z3<~_Sn=Os}AA-SKd)SkI+>ILI&)xO^CF0zV$5&2V|4=@2_Y~J34#WQ8P;CPzWGu|x zw|LHig$ouK?J12IG4d^c?Rde65hMQujC#R{5hLI7onC;|4Ij>~U$^IBPVv9i`pk%t ze>vaj1uV|2&A7>f+B{x!p_287?2U~WG4hW9g8mSAF&48ecOQ1%jx%$7z7CM zC)fG`i!iYNqFesp|4F5ZKtLxyO#hqzKb->oRK-C6f&FkYGP5uO=l*xA|LGAt_rF_8 z`oaH`Kz`Z<`7cf-F3un9f8JtZWc*L9cd&#>>fpZ`AfOxyNf99xcfE@&a09fF?> zBx~m!rr{^Cn7Ms9nYy{sPuJ6i3KAlQ3;Z(t#0dDo{D4T)A;CZt;q>sb8(?KwXyp<} zV{V14cne7+5_+p}!a0yzlUQYc`*-+qy46v0v~7KpQ_C6#?AFl?*v?IN}y|pjv9~h|7Y5NnA z#^?3)aJoRZ=ZE%uwPiJzACi+pfH}CdWpl zzu$B!Jwk*Sl2*4}rDQ?V?VCTYxV2TIOhu~&$quo$j~$Be7=FnQd;@+c9`N z>#M68Zaq?}IjrpL%M%mlvw6RlZ3Z*IxZJLEtPltOwqGRDxRSTLds7eolevP87CSRjQ%`qyi6SKh zL-VecYF)3V3r!0P3qiqN0p`(96hSoC>uo;k)mnz04`dx)g^FU?j6ek6hZ9A6fbDi? znOa33LP(^3k1rn)5%477;NaloL_y9Li_2-srGD+*kcm z5+s!%`T$?9)Feq96BMV-K+ly7vef9GHQy`kaJ$xG&wEr8>qhgZ77FNtMEvy_%=mX! z2$u-xE7#=yU@VcJpC7TA(SBDjZm6}D6LO_obpjd6`D$|wyPxsIEBD?X$XOx<6X*@i zo}~&0v)Ou$d7@xaO3M{1Y846oCMKIrPfrVzrJi)f+j6UbkHk z!e9*IHuOdu4?=ssJBBBvhUWO!Z8!%eV)sB8!hiuI8=EREx;F9oN{!#yBGt{GToW-- zQB7>dR;^l?PDSYsKBuF}WE%B^#6%+4uVSg9x4kd|Lf9(=ch%gC-LybU!O0B}OyEX~ zli55%vFonqY%*q6*2dae#1DrXl)f6Uv7l{}%k`#RYT?H|x)4@{waQ9b`#K_PJ3EB0 zVv|BxIqp?B5XPG@8QZOPa6?W+&tl0$@&F2W`1d3#W#0t?2z}(v#h(c+h)F+qe>}tE zat5}~dqa$X%USDmJPi!`tFWjj8j~sCWFp00k#KD|8izZ>bau5fqarH;xwUU^~$V3Sd1xZ=e%~NK}^MnHTjj*ozAn{niKV%^w!tnV#AFj4| zXlX0wall>TCJqt(<9+Sz8UOO-Wqrq|V%otI{uYnH<#NB-oe}ufX|dHUWe^DjHz0(a zCZtp`Ha1QqmkkysqNKW@W`M}?FRIaw${Fh)i~0yb1o z8pUEoqP44gx&|)rm4FTS`GB7Zg~Phe%n3w5@D<29VBh_t(umEShArVWLlmN&Tpwn0 z2gTyOy1CPxJ})K_pu(`8S$-k|C~a5ZS)HK3Km$w|M=Sz@9^X&TsF+SV!I|418T&rWiL+TQ z){5Oe1@sUm(bLm2VnW0svpYFC?bZAk{?vTp=pj_qfn;?0cNlZ)f?b1Lk^1OAHYTGe_QZqY{xEDG(R_osbsfe!K+-SJfWYWJUk9O-ufp20)*R|rfHF6`;? zv$V9_YIoW3y5?&|tc4;AvnG=LnM*pf_C|gBVsb6NrI^rWq980)_~w0G+1*ng7a=V+vC|d+MxIh6-Xn%mlawW}v5U{)J=*MTqFx zwN0G9TYt@0{WkU{f`3_XHJax^M!)>|36i_hxJt$tdTpr{^Mlxes)wcNeGen)pAg}i_YQpKASZ>tK5nP;gL zD1l-ZL?A{1ussA#uPZJ^|GQSWay}JT<4@V{=QN;`S3f2d@lxo_}XCB=#Ujvs}gp4MVXY2IC0R_b~nTta?1{&wtQk5ReoVH5G z+Z+1uk$PL{P$GfB4T+$jEyeRoi~K;;#dNfzR7nkft!kVT<4%sy<4vlcQUQW-wdRhy zi&bTT-x`pdgVsvMCg|o5ew#6VM4UqD)s=i9A}F`;AXgGc1tbF_`ndI;U%|Kg6!PhJ z4$^fpa8=3&M8V6y?m+1i6i5MIC>ohU;mDAp!thZPpd@;$a7-Y;ee;dKFN>q(#a1v9 z3M0sv>#*6#{y<#>E~>ReNHd_Sg#*(gM%>F+W_P{?YY`&}VP`rAR!W?6mA_<=woEB6 z1@$up4v%FXYv4s>Wl2De(H#aD-}qU@JIuq?AAtn-E*B6>-weH65Hjhr<63GdN|lnJ z0!%PRG(5QKBM)APH?=^O-Mg(MCiU*0;kBy!d>GKA2_k86pC$fJ==Y_o+6Pa{@pD z6j3mi+<%)QnUEkkFo7juL4$v*p3vz7*8=HqPm_hv0sE!3QVVKAUe!AZ1+W=a&(|!r zAYU0+b?SGb}+NFRFQyT&g7Qs9!y4XPHNRpbYGo}rsJ^7uODo2 zI)gnObD6wVEG{gFm*{HzFjGw54F)M{oM219<7M{o-@g&Z`C*P;>B-r09*CCAZX}#U zYXWqDp`CJeqKoBB%mkCXG@M>jP*!7zCId6HyH=dl9Im;?=_ke=yizRw0CJEfd0!~>eaPG9w{suDIYjPUDVtwTO1qmu;W z(g8iJ&gUZlZ^U*hHabGm1ro*~F`Ox7!8wJVv&m_vEvkjt-a^r}&CVE)o8kT73U7sg zzx|$3)?gpw#K=mnt%+rBvrRfx;qqQOm7dG(y&Wl>GR=qRZM7}x8GpMcE7D`-#&x-e zv+=cld4pnhDO5>krvpds3S*bH@L=VMAi2)>Nn`n5Wm%wKHOu98RI`hB@1aQB&2})! z)9by1I|@oTJHy2hA@g}GaU}M7C#GPNuWJ)m5;5cM;+?{^Qs7;2#X90T1t@Z6CoLbk{EZM{nyL8-15ks;#WuB`CvD~H>a!0mw=wIR*KDBE%k z#vkhGc5nORwT?O_C^F(yZr|#ETvzjTR&Muq1X+A86|3E$+JH4DtcbF-So)mKTTcB` zFlIlGl`m<8`Ux4|CeR|XRld%UGx(o;Bvn!2n+ni`~+AcqKQEeqh-+4^YF7_b)Q~j@-ag?@Row$0N1Gf-% z*Gta_OO$ZY$J31xI|{RRjQ!!Qc}rOkVeQPG!wq)~7Q>*RbCIaP0d1G-RoX_mec#^@ zO}BT5Oe$C11*eC_Y6D>qMBaYaWq90q;tAkAoq8SVck{c`>(UWN5qR^Ne%vyT z$dT}0VwxNI*6mlscFQ$ZY~3d=Q$<1eusS-J3RVRC2I2U-;e4-GrV!U?voel)+^6ro zDeQHhu2ei!CC6zvub0E+(?VIe5Jwb*(-2{lTGgg}w47@bD>r3DG$+#O@F(l?rlp7Sq7V|n1jGiiI^c_4$jMmsgf!{*<|e*<`BAR z&2(@ODO$%I8ijMIlnfmbH!Jf(^j}qCL6EYbY)+oOvCQAO6IdzHCXDLx~8XXPUo&Et|=g|sNDQS%$f$_QZ zs=#eydYQWhp`){7Gs+*=7&&^m=Hh>CJf7aG-4-nJ6f3hSD9utBSINhvkac)D%OJ3R z6NxYk(WAQDc|{!+$_fj`x;>aqmai6G7C}|f%OhBkti#6PZ@dAZy9b>W1U+;dqoM-?(+_i_$d*}abwecixEu{b|#a=;OF7@Va!pjHMGQ4UE!vG zvMGjHVKF%c89&C#$6e}xi)`zoy_FO{&dmjLWI9;n&=agI6EQMEc~~0Ff+`UBV)`0u zr5vKrAJ`$rr>A}(yMEewlwW@6F6>$J0)HE#v0Du=yQws@TtB#AijJuMB5d1>$UgJh6d0YH~mwOP>lysOJQ}pZX+9pJ**nIE={S;>A`~)z)TGfNNjPb zTT`=J?+oJDm-Q^a&H(QR}@z>US$ z@I1as+9vo?(I!eOE9q?s?qaB3Bj8|uNg<8n@r!3pm4elwDm%k`j4!NNtP7gehU44R z=e={MM!#pf%hUB5ckdR*Zj4J}D$9GkG!z<%JakH~&k|9XXjm(oUnQhh?;v0snN+Bh z?Pya;TtE$Qp<$Q*dhFE#Prm;86!UOaBarNIo*IaqXJffB*K-)H-RCe2M(;3KN&m>b zc=Mdr>}t_cD@YU`?;_AUhO81(Lt>S`dB%GF-rdB!mCp7s9L0QhjZ0A1ct8E_cEz7~ zf32*M&J!qbAa8ht0Kuph-NtIMa~p)ff3nhI_Yqf z%`G)0fY14MHrS}#wwb9|v<|tCVmT7ds{BhRQC%3)cBmu+Fdl=6AO~cDW+Ez%n&tTY z^DtS?L#gxZQd87xHe35zl~MAakY?T0h)2e`(2}uGR5RSZJ=JCmB^}M2@bX=~1Jerb zTfW%YRMtU%ADDSJQe~%ij;M2>6E6dmPD?5)vuA8AGQ~0lfFWVyVPWGdm3)0!SXnkf zRh-i?saqm`(9{m(q?njes&F@?8OoGNb(t_sr0=VE2BpE-ovcuut)AhRCcXrR_7ZP;yhnJ!CAHel>5z5=^j;U;N7vZ?m(#)|9qP5ZJ#T#FENU&1Y)p z{*ud)p~HH1`m0km=JoON#vmR#X*G9)dBgFrS=lHHCrPpYo*ll6OHd(aNZlu*Bsn%w z(boH>4pXRKOHiv*j_Y5nH=cU8Ur6?+C`R~vF|O6=xa3z?3tzrC#Sys zd5nayAhVB%R^z#MHI^;=7hiwSn+|PkD*dmB&Ec1hpz*M6R)=o&pQ&bBk7~M0@Iq0; zzAWn%4&krei?t{V&w{J9?Wr=pj3wLanSWFbLJ`~9JwAd2QQO(-ciMI{d(|VP*^k_A zk6m7`d;2eD^4a7b>{rWVQnaa2{h7b9#lU%jpJFGwoX;(U@vl~@+=8_73TRfae0L-mF-iRSOl%h0`Sq*CKgC? zEsVD&&7KMPJ?>6I%C>e9ELWvGuA@7~8NXIM+uIhSZ=76fogQr74DI`0abk0Z$SnH1 z36^^d39Zyhb##2JR;td&sF$*ve5^QNpX_UNOVS!H`W`#vk+Y9OeRvA=OLJyrP^N>& z=vXN$Rb%O0vN9nHUUA#k!=o}C}*P;824{d;{-Y|piowVT**`pXbpqrcTo zgj}SgvO&j6y%|Jm9zCwHvBXec-7)lS#MGbSY>k`OgPHR|Jh0hmG7!FPGhUq;UH7K^h(5LET4q$OA>Qz4 zal*EtWPTtLl3Grol_3i7?Zi4v1sOZqWT4$3|BCG6;=YqYcMPeJj2Zkd+6oAXBx@Vf zq6YUwmQSPY^~TSp9dfP&DJE3Ek{Q*3t~ACclMy4$%P{oC6PS;5@sS-`kGIGMuwS61 zkQp9ryKT>wmUKycZlP1t1+e8zxzLCZwW^@?hztGiwEtSYCWOfksF4EbB}H9cm4i|> z53wEC_UMtVd=KA&Cn$svYjq;W&tGUVZn*aC6`4Y5Ko+6;H?0J#58@(mC}v^ZqHc!E z|IpARn}M_@I($2ZisipP1&B0Ie+#jG{}N}V9 z{r`3@NP#5poAPSaiG5`V{e%EN)a_$d1x}8;3p6S%>nhk@hF39vmXobuLdia9YQ4a( z!`;fU(iKa%-2exM%smmmH<^#yWNG?OgkG-14}|+~6#E5GY7Z(gPNCB#%Vgm)CKgPq zSxXgY+j<)1$NgM^n8PWbp~}Z^d{+7UOVBqRR{4g6oy-!bjJy7ikNp1pe1KA}44s1m z>i1(eG!d$W9lrkO-pgYAZ=4gh5U$r>+S*jC3IXBog73+%zrPW3QN}+udDDgoo$E0^ zO$rQbEm#wJF>ULRjU4o{WlZ8YSD`Asz7;w9SdWU#4$vJ}B&z43H+5{ z&k1UhEQh|DteG)nUY@kdlw{vRJv*dEExUt3hFHy|?wuwyw7n#U9PPjHN|%VXg?gtt zTOLblMy@BPrlzLH$H(XZI(^@8v(%SuA#89K45qAhp2F28jEVH8>xYE^=HfpfN8d2! z8T^k|AH!1_z8$x9m1mY2-*B8l=E#4p2gyfs{m4e^K^w3+vR!g77@XU`SUIT{kB7E5 z1)evuJnZ8gom%`@o{lr>JUnf6WvG33yqymIfiLv<^oXq$?YHG^^<**Ih*TKx%yYlj zWBW5t%+IjeeyxFWy<>bTfFsKe@<6(~7p^wTj>L3%J)bRgxXoNuZ9!#fH`fW%wGt8> zfimdMQfJgav;h$SKve;p=8*}Dr45`%eed*7!cryOpMCxt9?YTIE6tYu>M>;?a)Iqd zMbu(Bn3^hDZ=857AE9E0dCPt@Ep2g+In+$5`;|h%J~lh`J}Z%VPq3HV=6eO=Fc2N{ zljp^?Uq;d8uy??@%rDFdFkLD2%tuY`R_o>diDmNPd~5HY`vdGUXGSIrbP*r>5f+pOMjI4)x1uF&r@MN=~ zQpz?J(?Z`%#uZhCRr`~H`GZ4`?Fj5oXKy+acJQ-NMAoycJlq9G)Y!~Jy`Cq0eieB8 zS|6uZja+Z>dmW_=<7zQXM&uc-OmF#IsBP{y9OEBISiKAA@>1|q9i4Sqjlf_3HNguZ zvoPbR+3Th&`u+~p|^0X zt;Kfzt>U(BBvwzg(N=M%oA&S_wxQZoXU*?O$Ww2=;3X^-erueLVMf-+{oZu4ie|-e z%5}Q=D2v3jE^8fZbuA)1uz~`^)_c2D+GZh`&G54M1m&0@Z7P7xk_#o7CB+>wKq1-Va+KngPhRfoWI(|*z4!vRD}zf%Qw8&g zo}s7yxx*-e+r{>2Uz@{jryVONJ|VNL0%g~T#Dml9XnPex{pq@A4;4#sKsGbp-f}r| zuwPr9VT-%@xmUI(m(h>FtY78r<)M6N!;Zh*X=t-m)W)W7N;m47y9J7Vo&V$*Fw9Uh zR9G1|3M8&LnO=5Y%^$ndz~F>-X?-Ndw{6R(x-(urLz?_BqAv zV|m+>VKa;8ed%u<3@VOLrBSv5D)pwX;CDiw0AS6L(havs5={i;ViqtrbiJa{y_XP5h$iX%t-AajrlL+hx@@Qg5@8d?ibU$##_8Vo()^w%!1Ltymmc@ z_l)DF51X#RLX4<~8_J+RvC=KU?ye4W^Cvh zYya9f>X(YhMtEEhKD_4>hFH!w&`gp|bX!@ElHP+&1ZY2I)8L_tYIN9Wmn5I*gVcdpAuf}POWbODbei#e%IR>Q%eNr0nMT+^Dk0aD%J6Ys z7F{7qG?RO`MddGl{|aQDENpGqo9GzdhDl`*&5v;B2na2sWf)_DbhtnNvNZ_+LKfR=Q zuv4PmFWNw@PigxkM6z-`Cmx$6Lp$9c(vEyS287A;2W0Xq`4{+rkXOm<-o)vWWVs_< zwx^|f4p9@|$K}8B^oStmvdDb|f~*G%S#dXUhY3lBZojzXG82(PGwqABDVpPI)Roiu z5~JMNP-m-jm!pfrK@h-zFCK`^1U>l}*)OA~EF%Sc6REGqi;Xl+BICO3-~8qB(C+F_ zhY^HeCCEys=UBUQj^!7Ykw6P(<;Z61)pq3e?ymeC!J`s?x$u3?BK$pBS$HV46#Dc?`k)z$>6~+1uh`Z;=aYZ1v$;1tMbDr>p7p7)xD)oDOTY@=G*wT7BM;n@jB?`WP zg2>2~?}^~kuT;dkouBaX^u@k9jr;68$?hxNll>Q;kdx&=gHMX@`1$Y&yoY&uMP$P) z8J9d{&bm0@a|U!ju%RyBZIeGHp#IV%TMji#vvcAymPu<;X`>vx@>ol4<6gEAm$)9P2sb*lezv6Qgsz1Lt{@1C+ z>dAa|eIs`<=~Sod(;Y?3x94@du+K0BP4QKRu1#rEiiTD@&h+~55CaqSK`ny3g}hv- zVh+1%V<{w~-7KwGEG|i?CS~H#GAqD?M`Ra4+2jnzt)~93X2~4pWG01*kKQyN*gi|6 zS{YEI>G?JXZtL|FIX;UlU>@-zTdImk0!dPV`p{>Cy-A`D%%}-=1yC8L0J5gwwpX6;)r~QwbVJdk%p0FmIPHgHk zV}K&})+rUka1J($=i+QE|L5B|)f~n9)p6>{eH$y0>z&=p zR5}dAy6oqJl44$;C`gd6VHGzVF$qp^*h?D^H;;d3->W{iYh_xoP+Y55UVarz(pt5H zigD6bGw2~kb@LVKg^j#YGeD*-s6c<~4UXh|1@89IzAh2^^>C-Wze^t_zEH#TrUFZm zKu(KfbY^R73U)hdw9B-|u7wZR&1gO^7-CI0qEJkv$EU>hp@-2_+#xTN@UK?%v_bvX zA_HY@My&oo-Lo40${7_AMD+r>&E^^RYK>SfkoVLI@NenCN@`s?*eVn#ifRk^` zQjRF~0h4*B!@6U-k#X;x?y-sct`i@N_FB&u@-qt-6k33%Snmdl3UkBt!jFvbNqQ>^ zD+c$t+0WFyyYbr`EYt<4=s13Va1iySQm)tA14%QrS(U8Hz&uwSn%P|&m|Y{P4Kz)Z z$YbbHNAk4WE)Cc$gAbp-X2b_pRHFSLFG}9 zv-#HV_LDPa_7bq1YL(8*I~1TY7%AR{vZNhusi2bsm@MyAJ<;EQ=39J%&$F+!&01pY z?_J0lJ9jEAUPs{Z1RPoR*7h?XjtKnHZAqaLsrAe1Xp^Qll=JfNgt5Eah`sOqqhtS| z0CMowJdSSYlJ(LW_eOGJ_#|dpov5z|Y>}ofUQH%ybfY}pi0>H}3X)SZ!VRBY;WUwa zl&!L4h7|r9O_cHr`1yEE&NoO#-1MAF4_8`rzP=x2^=qPlQ+QR+&l-3#)`&rwuq(@C z6(jAJn9OA3e6sTItsQP@BV^HnJjaG^ivxcVs%xgp4IL21m~g~Y>bp?V@$KJC0n}_V z+-H-f|DZ$?eS&s!w6?Ar`hDh+b@YW5^>gJ);bNGjK{;HY8N<_^8>v z>|%?!q$UB6(D+`hx*khy6U_F#evK)=XR~R4511MHULE*p-F(Y}+3xN5dck)^(%yeX za4FY#inijgQBiR4E-qFb+eK=+Eg7xuwr|g$$-)kY5L2^V8C1mZhFv#ayiL%dzFt@v#o*57vdlmb!r>7R!R4`5HVy0H~L69-0S_@ zaoE4qH(|FP0g@v5{WK6Iem$@I=OyhhZO5A*uP_xucsb3DhHZuccAY zhn2m2y(8YIyX7G*8(G@hVnC)XxT+5Ak-Hf%sCO-DT!0i;Z|6S_D>iWZuHM)OLVw=s zF9Mly#5XDrT}FcS`5z5Q%#grs61+UsJr3PFRP<1#!9JS)A&y{HiRn?ZyuNrDp3Q^$ zoI&|@ZwOG!j50t^kb$&7@uzR`rhATyG9>CUG3Z~)E+X>dM3Sr}N0GH(YcOR2S34NUw6H*l zhIwQf;gHv|<@Gfym+fn4f>~}R_%>S5YEG8kh87{V<=&gf4)^L82K-Iyr(S+Ap-N2i z(EJ()-0Od+sEmO@+*s_+rV4ZjL5pnC_@bAC@Z=dPq~v(}zl#cXPL@Wxv>vb%n3 zP=M|W|KWd;N_#~$@TBChs$;Qpikg!gpX7@G{Q5{SCbumDl(W|*Md!&TOXCcDVgu`l5Kbo0wKHC1Mlz$d}e&OLFqbo&!Rw9R3rwxYjlc-+_9 ze52YcDN*ojac{suG4cc}k=@sM-dOZCIDfW^zP37U$j@Ao@<)v;Eli_`r) zN+pIZEI!Y^@=ih_42{~+l-}~wcuY{(N+rs}C+U1oxA?Nl@9UbdC(Fyhf8%$qvD-)d z#~Ix&yn4C@@7I7Fee5d%P!Qz=N7IeTsDKJJ--{_Fv7m_hy5!DV-Fy8AMY~>!I+efb zR)zP&N+#YTZI~+S0az_f34J?L87xMCRm#PAWp~ z(I>IDdUK_z`$GQ6$w>Mbn%0khPOQ;j{B$nJ<7lK6*gv_AxuZH~dXrfsEfCGxMVU;- zoAxPsBz}*0e& zPlwt9gZjM}yR%K0JLzFk9ECAJYz?;j3#!W-tpFKTHRZwImc@5zUKD18gCDo++BKf@ zdpKfBpv14~B}{auk&2oj(MmPrjlOK-xWYvG*u~8&1-Ls#(Swm2h6F?an=3TOi_`P$ zFOrXE+Oh69C3TTxCgtX9l)i3WnA_3{c{ufK(i&Ug#v-Ty{6jFD4qc~aO5Cah z*>&U}keV@2<>1FRYXdJc3NJ6iL@I(o8yXcVszCN7y)V6jI3VqUfOdiFw?+haLP;)aCT$D!0na!hpo%+i+ZOH`{19RN&s4?U11H*@Yxp z19;rHuLKa$w0Cw#XGV0k;%R58r2q(5I6S0-%lLjlTa(|A?%D@6`jlD*5_#Gi&s&R7 zVF-8;)3lUo&~T2EKf5Q)mjXyWO*)7-4kO%hAi52%^%W~Wx2Vi4Z?{LHLqL2jfv;!O zD4C?=*y%9s`{rr^2wVxbjNs-WKvj3z{N2$ z_H%w8+c%pYHJnh09k0&A>|3nNsXM#r;~2&xFm5?#ILAEM-FSX9#L9Na7&4@$<}$FM zQS)HA({`V{jmM69(M{|sV_|A#Snh`1k{okcEF9cWvW>xu5VJumd&ypG*n->l8KL4& z&c;d=4*J0*9l-RUb;ag+KiNz6divG+W3!4wnXOX3#iOMA47Kq&Y79o*&QIohm=-*g z2)EGi(`<(~iAcWPo}6udq$J|FCKL*CkY~aw8U(`#yJ=cxw=jZ@H*)xG1wG1;F>kLC zex0}#uev%Sc3k?9mI@vD*W>;MKN5%+#LZeI*3Y%8`7#xUQ3th}IjiVak->wp34k)M z<*%T>^*<3eNklcPeP8g*!6L&|zG%JQkRqE5(Pz2FU;z;l{ggD_kNcmc)o>Zh6*y;y zfLxy5Kdi;|U&BLV>zax}B4?0~9co6b(*4r3RkKSV|(0uToz_m>hW%%p=g_qnIk9)aUs+BHK|kG-`VCQk?b2a*7*k1kK3I zO4>q@&DZXm3xkVFJ|W5~zyrJVF>pt;2w4t*^{FsWc$f`J zSQU#4_V^8ocoh5E+|qL*UGPXYXkPFSSAe~12)+8l4?7%KQJ&D|*WJa{RrVbl-+zcX z{}BC*_qI{WPzR{%j(=g3glp~ic3#gNMmfkg%kzGm6sMD&X8x-}antw3#mI5K2h8S0 zTL){3tIP4e@KRk)ehTk5ni7%Pu;1M8?t-D+qcdSfN_+d@z@PhED4;Lo>cRoCu+o1= zv6e`=U|Lz82MT{+Sa8s zPgcDq_STsid7MQ#7p>nwumnoYX{p-YMR^yv%wPG*|J?H-rDy2o;`|t0ee}uVV+>JL zDv`^YCFDp{d^+%rOtdqfLPWsSgRz%Nc|9>~$5xRAoT@XFchiWbIRT6!PkYvkV+km!Hm5ggm0M*bz7dOG%{S&T zxH)|`@0~aQKUaLy-<_ifDKiST;NrKV+hnZ-q z&OyDHSO`t9Bd4UC)VsVEgV?rgt}?;@yq8!=j212JgTqmJcttwj)#}G0Jp?T=;?GXk z4_2T55KJno(qK|4%crL0S?xux&HWb9S6^7qwF*q-{FZ zgu%}N>7YdMD*SZkiq!C6cVBK)(nH_IY*HwC0?y@xPnuqv-nQjV0a&-Zh`u&9DJ+zi zuFf7ix?jg;j&8p=Os0{I(z1tU>HzF?holA^9DNg$32|`Kg4nB2A){RN2D5a39_QDl z28%iT3SS9C5(l^$VuLDZR5IS9y|;l3SfT=6#3^Cn)B{j8A?P<5o_{M>WImgvHK;Km z#ALjyaoNRZ(5t6BOw6;FZ18R`PY{HDZH|2reFd32#yY>Aq}CV`W|2|3&8Q5kl9EUm zUNz`<+xeZ}7ZPlEBzzPUN2)M*@NKBy4@%0Y`kEX|Zgl&shqh`SeRd`6HYI|f%_X1e zkvINaHRBI3i6sk-@={IS6RyY}p|{%NdpUTG;SV&6Wf#-8xm}o##l0Mgn@#7)X*e`a zDWJ~Bx3{)u4w<8cP}7LpIgqy*1M5$5gHs?$Fph*-UW%q{v-gr} z;C79Dwi|=BTq|-Z%$ifEty1r*kAEKaRoMv0F&InTLo3$^&TyXogi);R%L<7C+W=H{ z9;jX4*a+xr&yymvMrLG>k$I;D!z2om3p`HOO{|s#X}57oqe>#IG+$h#zW}5e+^$x_ zJYEX)vKuT5ld0cnU26W8GV>mrMy@|xs33SkB*woRvn5zPLx=R?k$N09tWBM(K zpJF7qhl$&WRKLpuBRyLWA=j^o#hOnXDh*QHVyis?#oSwTpa;}fw`>ayM*e6T@V5U} zwHM{+!tPvH#Vps3G%bNqWe$i@aUrD1cyt}<1RifoH>CERWd)Qjy-fq4 zK8IF?n10TXJmr1kSfQ#}L0^Ow|F)kC5+lE>)0s0{c)vF-k{l zIGrl_@hRi`uu&qVOd4#dOo}*#I38S@o(A6n( z>b*vmj45l$xG{69f^lxESP8F|c6iaEL{i*NcCEim8dar&wZRjafZkfkQn`{wIGiDQ zpiCjq5h|cYzFh7no;H72%x9HMteB2UGA7@yiGl}JD#{y=*55G7EEULNMWw`TGCA*| z+#PyV?1hD%Q#6pBM$%Bll7PzoK5Pw4iy+3aeNCiDc{9H#QKUtqitS!FgkScaF(89u zS6x0#st~A7(16?^Go`IC0u7?Ni1jO+D0s7ltd_l>w8-ORhRKWXuY%yD@lL2?A{9TJsno@?LbU^+o`GmAH>TCRsyl z!C~Eaomy$8Ya$&@9m51neI1vZiAcJ6 za$Z(BRTnG#kI7$4BPAaC8*NX4yO^7aG!E5-oGw#(RrvC2GE_Vd#-UM~Q)H>EWpiIU z#9;3i4IjMBGaDh35^bRJ*+w5-5$k$F?-zo-y`yxwxBdPgc=hp^*1ee9|^;)cXF)S7!aa8Gk*T=-OY; z8(RGP;_B&(r2CqeiGutUFWo1E$E1?uVBYWln@Ri`>7vpn^=zH;mmK9*HH+&jC5!Fn+vq^Xuigmh*PZv% zp!TGp&5h=J4;k~DX!q(y?wMu@jyDF#H~jMPcb=<@>(j45=k&9$WT+c{#OQ`phCP5F zuEX-VxysQbhLGptHK(h-oAE{oXA);&=PFrODxKYZXJC{L%T~%Kk9FI1384#@*=yx7 ze>gImiBYTakfI1@v2xhwc`~uokbC{rK({Tj%8A|lwc&X5 zcSY(rdgi>_T|iEv3UzF`MWFO|;{Xhvuqrbu*yy+wE0%V) zRLN)^HEvty+l@O44gPng>CVnid|@!yM0miU`+j@;Cz6h0awtI2-g-?GqJ8;DD$%z-B#IY% z)oKWb-F(No$lB^E7c-#Fz$`!6%RYS)K=8`!J^vDymkma*d}A~QGbCWYnc#pFDQ)sq zt##qiPHS_0EFv3E#*db!;@2plVdZXlNo{1V9EJD|P;Zr+IJz-5x72~RkXQVJea}u$ zk8&;w;MDo?o3ITX7g+5dS~-(D4BpY7g0$j9tx2P5M1V(jGp_uW!l3}%$K^VtGB+$q?iM!{}Z1f?;6Oe4!RLqk7p>}HQyOUyu|Q~m5r-E}8c z8oExuCkvZTguAurcg?^}da}M*i~ttJQb1||YAZ3l@6-{Hc<>QUa<9nq6>xM(jjE;# z>mb`hNeTEwj`9=|Ew4?MXTw_$A~)3Q+`4nfoC_2tCUdesImA9D%i}wFo%hJV)(&_>B%oUDlJ~wt!3skwMzKz?COdkVy_GfzNU#2HXE>(1D1!=p9j5>pc_w;_!@`zSD zp3D(X=#=++3E{GpZ=cDLbez3JbHQOU09?Kl0sjeV2$KlUBXA@9yjl61_nSZ4<=4wW zh3vaS_Ta>6OWV+Eju+F;d{@>fWy^3qP7*c_{b;kWjTQK3A3H0znZ=gaJo+FPd&lpA z_x`e^p_F47u(TXxp3fv+bxVT_FziRx@T7eX>)EbPiZ7!IK9j@VN5`w~B0|G`U5XK) zz1@62{9H8wT7)4Q8P!B8jO#BQw4*GYx`GkIHghgxX{w1mB`n>;UDcvVU*|efIN>?y z?n6#@2^r-vZ}>EU5Zm3)*Q^p#`ZY_l5T{``SZZ`9yiawUk(pP_jClrR*Q+^2iSjge0$4}b)Fv3Lko=HeTz|mOPV>1`^VW7ao>uV%N@QQQ z=5slpWlVz#WuD66eyGl3;qB^B+WngDtPhT!)UbIPHtJJ?@*h#ajPwLi^$Y^V7vFHD zC@Fr+C(F)P#HK1{=p*>L$2sq68K@+Ck6*|DgGH&g;cw^Im2ds!ztZcwH6nbk-mG3v zH7NNaRny~yssO3J?#OK$LY(cltx6fs&-3$~cf%;I@CZQk{BvZ0Ou2R_Z4 zmR!b){^HIyrw_ZU*TzK;5_&t#Ru}OY8JeOX1FAyh0&)V&&C-zlPG|NVT_~OYjdI2L;82#s|1M-nrwQun_L3Z9psD{Zr}pi$t2VbZcF@-=)dhCh1rQ z=p*6#Em2C>M3oCGOEd!!kiuX+rA`kpWjr?rGx~r5_6zO&Fv1517o`5Jyeu5*?6ZxU zQ;B)*ra13DHgk?CT@R-A>ikM@8|R0oW~|6xx{?MhVx$df`^{j)Xs;{WC)FDxNpJ-U zr7Fn@sqtt)O279-%PQ&`JEj6>=tfQU+u`eo#!iZ6OcZ6sz=ki*GcGnm_VyHw3HX-9DPF^>R9~5o{;w0P(hoGVBSTQNg>#O zUaY{P)E<%F$<`j+ukGi`JZ15C>bzFSxDc`xn2NsT1Y3SFf}vEpFAQ56FZVeg%G$hhxrpnl7cvLWV#fQbb`{8!U`*3CWxfM(3B~ zc6Fb;^OP#MjM}G^{^4kA4f}>3`_&QpDN>n?Y{T^Md!61(XGB}zPUqD0cm`A`(6L9t zCvB}_1@wDgdE>PXzOsL(bh_9z=7X9q@9R~5d+pQ4ZoLded6TK~a!_jhD)>Af9 zt!Zy#NfMd^R!>T#p7tJPa1v0DV2k;zb7*v-v`vp8yrqu~Dp#f;-<@g*W zZt%chyF9`)l3e%`_*b7hedtI074E9z{Ig;=9{;skzHte!l<9#M$Bd$eIl}GAYy)eD z`lqyR5u&aG>B~Z`@@7sMlby#j6lhxKgmJ>`m{n~kF$SBnI>sj&!*SEFKB&`BF2^~_ z5J6bG?PZq%-$qV9FGjnE^7f`*G#b>ABeHd7`D|tm1TuN`Z9{T!oUe5}F3-MqgE)PL z;Su>R(*la|e=a&4Qy#~Y4gbr3q}TAFloWIYJudpQcs3F6H0`WKR96>=x!r;d?*Jy} z-%_r5ZdqXz^k^rj7k)~^N36MrKCLGF)K^JN8Ix$J5TL|_6Auf_sP9C6%L}Mr=8Iha zBa@xCVPGs?I$?HseoRD8rLCSeL3w@WgnXTi25>M`H#YW&k<-NrtT?;p&1)@}xH0lz zA$Nj9aAL>ZgLF4$d?EYek_{$EKQ=sm$k2%2=B(u?n#_owal3x;@9N&N`#~;cKMB1& zk~SJ{{mnZ|U(JSfBm!J(=QrftZ*3@!e!$wj@>z0-4jGaG{9Q*~LB-iKY!l z^t^|4n$#4AI^}?P!oLowLelR6ypvFaxSA}V%PAhpre)wvd9mRwO? zpXvPM09Lrj%w4c)&Zy}8`+L@6ox$qR-+0B+LYAQV$jVX_8Tfj} zHiAAfGY2;;5eVqQV&}1DUEV~q6=RpOHitzP8vk1FE8Hlcp*BHSKA2(tYLcYtOtgVU zAcZE<$9W8lcIrNiEv?((QKV zm%h~#7mJdE=&+s5ej~c0Rr>UO>pSqy?BnU<0P`>zzFJ%{lLMNfHggWvEta%b1F?Al z%j|4IA(3RJ_tF|Kkbd~x{Y95uOARY9UR+KuJxrdXH*Jrh6<~X5_q#PA_l0fkQ z-9>*+u|+BmAWS{^z&}Aqze2?S%9oiYg#SQw#X}a>W_pFy9u^8?3 zSl4SQ6cj{o?e#fhiKnz#1hBIk$t;1F_(8#{!}c++vg4ziY9bdCvUz$#4p!s-36zP8Z zkH6eZL-)x?-Xt1*pzS&#AV%kWKW@(@XmdE{fie3&2#5RrvLYEeglXMeeiYQms9R#H zA9f>~;QuvfcN$@|Yxcm6Y?A+DC;GqxU8#}2Eg`u6$653j045k;mN-CTYHRetQC+Yh z@?S?~;!g&KZ$qubJ{d4Qd};pk(>5iBOx~10y|Fae1sO~vm%6;oofHTJ88#0|YkBi2 z8;}s&``BC4R>KP zc;mxYgAIuvi!ow(9mtmA6P@O0KMIYH56L^bW%Drq3d<4ZEl>ALn{g#;F17z#0Pfgd zQdd%J!)lvr(r_wKxy&w|GVn95h&{{FwbrR22L1aDD>+LWrXM39CLwilIL@#3z`ITx z{QHT|mxp0#{!a(p*5{o^x-EKlV>WqD?ftQGnzIkg{le2wI#YRsc zTu>q}4G9|`O?AzLnW-rQMgsh#5g=xIgocumo~8DoD%v1f`M~Hf*@96Wc$>MSkfd{F zrkQ-l+L+s*#Ngtb3_F1%Y-2)iNGmBhO+`z|#=VSo6M#i^3|Xk*sag7mb^wOBP_dYt z%;*p+(LzedQ34a@73USfa6K4Ll*Gc!SGx=Z=lJqf2dA3{U$!kxQJ) zeC64=r828hDX13sFOZxmd?ILX;w?ZZO^fX6uBM-{vx1VMSgn{g$e@H26&m++Ni?@A zq_1Lc;qI;PUp#YuMh1x?vk7mD{rSicDBW7?u3DhXjj9P7KkPnXUUqnC26nEnSPvuXXq2@BInq6z9@(v&Rb>k! z^LAez!OFxOoae#p7WAk;N_Hl{V-BQ|L~yofXc!VsCn~64 zr#8}Xb4HwDIHn1@$(F8Z7H-Lg$FuP^+6N2ccQrUP>zwZ4 zf)y;;HA7anmdxwpVZV6IuP0(CV8m^6x2EGE9^ ztK?Ir;F@WCa(c{n=^8lR!>;tkDY{Y|9$)R<=4(j%#zvKf6Vh666Vd4wFPSZvOjM|h z?>pe%PxE34WFM?S-+W~|akI&ZiN*tc>Ny-jvD0)x&VG))*wtCB zeRVu7(x~0~@-8!}rG*!QB>s9c-d1#|Np8X=g~wN6u2!P^-awq;MA#6CYN`b=R<%7! zq`F+RoY6|W?O$1FB7C$ap5t|Y>Ns9VTYuvtJ5Ir()4}F)+0A`G`OvXpXGa}((g`3n zK&s$LT=QDl>vFaTW;KhEel~N>a>@EW(YoXt7X7rnjFpA+BvA;eKC9BMJ2l^}ym!E~k9HAO0dU&=wEy#lRTXKbsq2*|E<(an`%XAM#{f4f9Vm zVJFPtlAL$wTY67l_)#WG@ch!|-EMQ~?8>sP?tI~HK1MuPQb^JcM0{}Q-v#bzB4mk!kH3qutqJZYuq%5!~TDoELN8 zm5Yu`Ll~-<);k|QhfDRU`cv0+T7Bh*8KsG_8i=F#eI z%;B?U|0yP_+l@m=Ij;;tLRWxq-k>z)rl&eTKR(sBWDX#?INFQ{o=B^mMD1x7b1-PK zy3WeB6GGmxQ;8?|v$d)&1C?60$REd}k~eLG&ZMj!`n~RV9X)-tm|c5!CSjuS3lX`~ zM)D3_*Z+iKF-X4X8TG3?aE`xsU`&1@Vr$j=9%+*ON79F4wiYn*k>8!e=Qy-LbqlQc zI6i$0R7+^eZ6NAwz772M#d{|0<3l3Cj=Pt34FB5lRZAFDTXk}mh#6=L#G1@uu^hT` zk%_I^X(c*>2*hTLA~@{8!#hm&>X}UJ{gFNx6{wy$b1Z#9OWd$Gz`)n*Q2dQB+VTKo zOTE)KCiuNM8^YBkWD8T=kmyG{j8spSLfTr(n%``K>0~;S_d%rcS5;nzN6vs*L8~aU z9%^g~t2Z7cvWr+YSzl}uEZ?+Blc%5MeyWC`d>%a{d`Y4qj!8dp*1;l^@rX=8%4i%T zVa?}xA{ji~dg#t0;Png-2u(?ul4y>_v|C4*<<;!VeGx!6F%)%sIwGLr#88*HJPukdCoMPsS|`i zI(vKZl(z&Kh%qwa4QX@p5AX?Mgi&t_yz?oQ@_oOy`G&wxx!fy=??^90haLv8I?Tbq zN6nUUxX#z{FjQnN`oM#~Qp;5&Mn?+M)}Yjj!}gu{k-X_9@%567Ztf! z73#jsIpwHk1#C#1v0(84E2gXMc--Ta!iqme3k6x-4=u|*oys3kfxX)E$t4)ypEh!TRxBR26ljE@~qL5J?`w^x?IGm;t^Mh?RH7Y8%UlnO-Q%v<( zok@Sf+l`;SJT(OIL|MA2#~C@D7F#ea_&d`v+rPusc_c-+q+em*6o+)&Qq^eV3h~$7 z3!kI4(ULh4b)9L2D-Sn_nACKz{aJ+KVyWmN0nU3`MNpuZZds21xG}kYhTB)ZuH58E zd79Ac)EW%V1rLvbK$MG<&bJHmI&e_NmNJ00~sBY<;y`Ol1%{OZ1p&y<;8c0}y30qJ4 zL6!+I`KnL0IUI(gbA{tgS+OgxT;T$qMDt@NUo%7A8I zNzAFNW-rmxcSWIf{AaS6?HJa#{$m;vgW)OdE{DOo!3YOadOZ21;G&{HMZI5@7Bu!;G4?E1Rd-ND9o0~0js%56M+PqF@-MA?6%?&Gw(KEiOs_UJu zTAr0+rmbO3Xx16^*3E}8Q*emNNr6PJ>dG(#q%CBA-s*j3C*7**s6_Bcb!;?yJqEdt zC(~=&@5R#&Y`Q&eycnMO>93z~obfu9_D4>tqMK+!ulc|+Cs$~;?i`*%Ce}S&&oDrn zp1^~uZ$7khA$d0Vb7%7)d8nVWAbIqbCvgx&!abG!;7Hj~1N@~Ub4I}1sRFYhALG&M zOakLH>EjarLb3|07s)sb?^ej$2E|dxINT!H`_FJ!qWE=SkC`z$SbC!gjl|=A?nKHD z7n%wN3@D($5MRiXZ~(%1;*rqaI6~j*L|Oz${lqx)<$f5z#EZzIgOTm@fmVMbOAFwu zjg@fB(q@VgB?mwc*#V4u4dh&^I2%?5uJpg>OPMSW3TPJ^0FC_mw&hSuQ*qmFvT30A z%-D~fbbBephzkbvGi4o-FvWlFX`re5Ys``7hiUHrfYAMNObR3kJoQKdh6|?=~J8(Ii-Fl_4iRQT_T>;k`TGgkeUtp<5dMDs`hQ7;|69ZG|FDy8^73(Pf88W|>_K{rKZ1n< z1iT4~mQKG6-OB@QQ37unH zPq3AQ#YuwU)_%Xuu0KicH=`e>Dc80G8gZZE(k*aW=QMhdVF&FHAQ&NevH**9G|p(t z8%LI#@P^Nbc66K$dHV6O5lYjQvX#Jip)knLK*jhz>LTQL;4%Pe+`@5=MPi)w6K#FK zzKIt=LL+Mfk)gT!p(`LD1?By4J1{VC=#KMn4^DH-8W+?=%R$m9_8Ok`NeMBk9qp@x zLCaD5jlwL88$j1MV-~Qg{-_*`tj0hu+eptCT|3kl+2EBHP5v=Z8T#io%b!Tx+2bY) z&QvCc-}&UO^a!`q82w{-|1ariVh&a)!!ua~>Bp@$m;)*4=W-Ny7Y%7hLyLgi!`9B> z_I9&*(TyDPcSH5-eo3rsbLZ}O|4-PNor&REtmzy+$V@IzwkE%~K(YfOHF8UX&a2xc z#U9++p?6G9MATbRd>k(Bl2qP9o`0c$j18J}JCw!gWe7%aHoV^^Yq(-cgq4-pnj`vh) za<$x&*`m>7NP!}N{ihQ*I3ZNOTwC2-qJ09r>0wIg zA~!wT(3faTfZqfblrN!JBNE8YaB!A=TRSLG2!x;@FAxG+0ZaE_8%4&`q9mc$2?_(@ zAeY1|^UE7_;8_CI!y8fsh#44?j`Ox?%lBd7#hta{eCKs&jYe>`alpO21!1wj3QjDu z@Hy@B#cZi&7aG_4Swm{gYw?kH+j-x|tbmrziSp1XDqrO@wJ8-ZjLO4Bvqqdo4W#=D zS_)V8gyeQCC5tHk1np$DQ)jyrbFSU81`*hNs;tE+lgO_<1Mqp8t$-drQ}XR(JKL34 zH4hQ!_`S>4O=v6*D^RXYC)E8Z2OeMH`_wa5&OqSdXF2m0t#yb+ok&)6%wj#fD%J&R zKcGP#iDwd+k>gL-nSH?Vt>u9`A*}la0?&pqZM*LGQ+GEPiY@waKZSN@%+!{IpDn&eeVD}j6ouC-m4D5>sw zn!)Mjja#^sILP21@B5a~bdv1&&&<6L+vjQecL$kb$##2(`GemvY$CrNkNbml;XmCs zKj<7^yl;2ug2Q)o1qFzYcFv%OqqQlW7&x2EPs57C_J7rCj|M^yA5^Z^e`NH&tMG~~ zzzM&A_7K=C@Dj__C%isEK;cb>%_i@afZxX_-8;_!R@fTIJVi?*VbmqPFCoI7uqOq> za+mdR+c8?VjyHLP$>I$Uc>1IUpT%OYbh%2^5sS_Hr~^2{ZyQc6N))UO&ik;>zoHs) z0+gru7>b@%F~yw0efDLH2g}=Vh-x>-yPhtDVfs|GmqVk^a6{GW?UpOyvcAhsi&g=N zaxh2K@T0Qygxz;Fr03fo&KV2`7&yXB1a0NemsRij!`ansUL7ww(TIDXim|EX*U^^q z*N4higQF!;0#l{sZ>A-ElEaAWw0$<2ymgN~&%dBNT`m?TN)?;VHfpWjdKN?DrL?M{ zWey~__=$oOh3t(_TsLi3nmpI|X?LF#A6&-3*rUfuRfRbAQH zS)mhQbd-$`35>BbWA)VxuSUHO@17~}&nByZ1&I1$awdTL-0B2J0Ax5@glcK2el5m_qf zNlFKBAXcG6g=MUdi+^fgda9U?a~|EIoqZ;A$8w2$E(xjh)9aPa{jG6F_%7c5LbpN4 zWcX6@@X+>DsQcvubw95VM=P4tQN9H&Syl~Lw)9d<@0S{1!kf4I_xx_F+T|936n=|Zr{t`OHPA9z&(iulm zK)d(NLaQS#CSMtg(HI3EuXJ#VOO)r_dH<#9Wai_1CjE^dF2NY3RHUg5u?8CszxhQ) zspK_we-*jEHw0-!nv_)(k;i!(!T+z+}^8oY3DANYG}n+*osy3ZzrRm=S|scUfcIXdWw)L@j~;BAGE|j@U8|& zbb6OPe|O`3o)f+9!6hVbyy+dp%oj^#@p((VsU}2sjc`PeqIT_I<~B2q&p$G}ZPZ`3 zm~YCa)mr(uu5^TX8vYRk?~-m)D4=HCIypMUUOsShtc@g2^qg*MZbEu6paQoJ>gQsM zFI=~Nm~=%eW=Q8++)wxd`NuH!!yNfn7d_7c)DtLZ@z)CmuL$n@1A@>I93*}Ok<}g+ zS2k+1dkz&)NhTxR6LQhD4NS!LMdo8)dUF}gHI&{$S7LK0Pu->0=m8UruN4t_edGmp zD`YIKc-*%msT8p$Pki1_(uXa6V%5Uu9Qr~jrBBGVITF9zt$@Qd<1$o!G^nTMZe5fc z|3os*h3UiO@o2iP+4ve_Cf9ww?8~{kd{HkX>V%a6-$^69P2&BTTbuyt0_pmQ7>xD$ zXx4TWBQcQ>AREsWTgrG~amh{5Jly!Zf;Y_93>i|={TTv+`!A{L&>&YM%-|V3_seK@ zWPI`KQuT-ad0Req9Z1)qks91&Zs}--d2F~JXmxkF$>SvNquv#c45P1OoFSL41=Q&>yhMgSMjigu~uC@5aj_Eu3J zdTD|nIYFPk^vvujbjHbT@3+IaP%rSOZnlQ7;;(Tlp>#Ar{ z3!WVqpAe#OoPw)(U@_Cp!lsxaJww8%Rq>OZHqr)H);Bv#U_-6!H(zwEbP=#p< z`+Xs59z;2p6K4}Y?pYw56;md&e75wa3)T4METbCFarvw+t_nYB{?-xS)&yF@`h?yfxHkg1&lk)A@3Pi~yZx;6`;( zJsL@YlAB9a?Uv2S`#eob)zkC9F^~HJ%@^GxV9keZ>M}kW3DutkdBDIE^3*;9RbQz( z9~Wq|&X{z{s9+^NPJ+HwXSKL~9~Y?UHg5ucyrWxu57FPN#?x0R`e?`@4QoBy`ELuT zJ24)`lxzumNecD6x(FZSpb-cu`GV1`ay~;q!ztEfuTKtqKjC2-d#Y0% zk9Fin3A9f_DPOAnorom-H`6;kYFLFYS0A2!FH?N1>iMc^G!5IHDYJsoqf!k-TfRd(7^(V+7W5v5qMajN-rBG zi}@1mKNHFMrocht*@Spt^KDao|1<6=EG5_@1pW*oyHHE3noY?--LKjKC@UoQt5NEb z9To=zIHCT4v$=`!r7St*IpT)RAqvHIQP3LW-T3}#PHjO?+pX+(caImSpruRx(#sBy zL-Gr0<{yHMur(@tF9^>@Gk!>2>aA^eJ7*rGhnxXqoQgf)RA~n#oM|uLg?iPK>uiCA zf->T7&*dxPn$f=OpbYQ*uMBqLW{~eb>_o_54+hcT9*EY+***jSwiXH+#?4O$#|c7oW({E~FJ+Jq|tLrnoE}Q^v@I z3EPCkUlM$cR!x&qFDR&f;Q5vbkiC$oD+TxWY40{#e=`d>a3OP5s@yJEk{S}B2!;pA zM`?<8^e{nW+>+X0xk2YT9epl|Mc+DW2Ra0AYfGDWLlY3QWMwjLn&d(J$!T=egVe2c zr#La0H7Qp0W>V$3Zw1F!4Kv4rTvX^2OWZ5>;%zt#q7VYSoYfO|t}URe5j1{4B_+tw z=x_hya)(}wJmPhDGVN4?cGYG7&G4v!oqcYOXuPG+k8yC96(kv{|01C%c)7p+IDf*8nAYvKcN<0;csUd>KRGc_YS zp}5ZJ_LKQ%ICCm?H#b-yCi)aendsM*hVtYWMIPCY&%L77{`T(Lz8BtjklPHWfF(ICuG)xc zE@*Vhx<>@q)ErmWkUkEkq=*b`bR0`dA{oj)_So_k6$PUx!4xn_GfmYI3qTMqkhHXV zkMJstjty6Lw3ldb5yOX;`jPT(ka1TQ3{b|0!z?8#>j1`a=bur)Eacj>UJ5;bOWR zMGY{pzG1~mBXHo0&|t7yF(xfOs+?BVg&&CQc4a~${epnK{4#LvsLJwJ4%+jb#yHl| zxCc_rK*a@{{1;OpuO%*f6O*~5fn7B?t$!Hcry+|qaK=5NPE{&$&qPX1?Uoz? z#%itYTExQ+3u%p?B}bdvl}i30CY)R*7PQdVt+w(bV1m1)VPmJUbTO^%{-J;UkBK6X zgpX5#)%sAieu#qMz&FRL0r%s!XhJ%}<&xLy=ISGV81dleq#eoX6OT={uk&RQ_^>Sf zicDnIa$PO1F|F#lD()5#3adn98dzBiJsra440jWBR&nU34WVP3dNuK!uNjl$S%!33 z?=ILGIDU8z@P2qozC>ix;HeP?$F-Rm1EUXD)aKyX>|aB|?~G5>645(J3N;gLVHGzyA1x4$X=%uX!3C#P<)FDGnjk;Sh+Ho7ZC z<5|jdf6zi&3Ik*Y8b?kuk~|d#^^^_=Nh8cPS!p>w?bW_(Vx>$!l^WLM2vA7+@v7rl zmAC1ZvCmxy=Ap?>YVp|g*X~k0zBk;5@KDV>}sfZb5?@vUzH8mJn5US3#5_5U^=CV=cl@{Xr1p+r) z&+P2t_vgTC*`Zf$Ups3i>^Kq;ykr}Zm_pQ$R(``ut7g%~0+&YnKE2Ho_=vyS_PJ0c zekyaghf72ynHH`<(3e!74Avj)Av|{ng-}F6bvLuCcCKHG>srplin-GKxPPxo&~atS zW~95w%sYON69va~HZ*`y97DL==sFy7`4AIk=b*>N!4w5!!(`WMt>MGpvYaS)dV3%w zdSmPgwM*Pi^omJ%xrSqEwg6fq?#WYM=r~`>Zs=dM=#p%_X|pvsrYs+FV5+wL1ll7T zBU}eRIe!--iZ|Zi#i%^ysfdUmiBzD5(@grbJK5x6k%5h8m%-1_f9(%nk!O|e)POi0 zT7y^mQ>yeu4aJEm6>>8Uu#8BADwJ6b z)cWcD);wAK)z@>gu~M~_z}5a9{?56`%-AP862tAueMJDgSCOL~!K1*sMP3EpjZOLyRHAXuP=BWa-3XO@vbngdtSd|@wc;uy zWE(PRQSyFLsnz#De5aG0e+F|5GooS^uyZUJw=R% zGrY9czX&t}?inYE;ji<8&IDP;#+sgA0o`|I9_4nfFUI$F@p%noD{>~8HPwndesh+$ z^~}VUku~?%w~mgeN=}5|q7%Z;8WggClrTT8j2oXNR!7IDg(GLv|D7?9jm}T2OT>r# zTL3ZhmB7=(l;H3DAaEP(98Zrbbazk9NNMlP^6P=;R+kZ}jWf23tB=vmEvGZ4pPZhn znc4uWDw@kwDC{8_8JD4Cq;a@`+rxn!78{Nw($&O`G_muSS5crtJLc>&pR+VyJ|9K#kAWiGL20(5rxj zBFV!QN{`nM`aX%L{_~IiPqIIUzcz=TOPj3|HF@5G1;MxsAnA16ST8dZ`iM zl>PHabg>9NW|nMB`)>C6&;$MFP(W^el6^^)7Rwqa{6!LoAljudCkEMxeN!rCSLLVo z4&qAJUYg^IVU?k|%o0@u&1yK;3O2MBdLSu;HGLN!%a!ELng6V`g~Y9G<=@PGr^l_4 z5H@_>2U`qRsLUupB4AF#>_bHVC+wI;A-&g(hOs1e@2UTH`^T{TqcP9?x*N>0RRr`5 z&yRlFP6a3xA~`@8pwjqtXa%yrV?8Lg$Ki)BtEyOL&shCbXs9g!r@hD-TsQodzg&-w z(05HG=*x>-toeXST1DA;Z66^mY(HIhbCK2Fv}YIKT^`bC9oBFR9wN)1Su7qoDGx_N zQsYRWY7M$;C?WD$x;bBcIq&bltBIiDPy3i;sUv;(NN|=^P5KcSb?D2P>EO8^K2Lnk zyqzXglMguha=PDh7!q93DX=O{Pbm61<{+c#~|_F z-PIK$Zqpw?JQl{$1R}9c4Dg)S_uK;%xTYCB^x9;0WGe|uf9owOSIthM*lfS`8tr(0 zsMH8{UmG)|&syZVH$U?*wW$nllYL#XM-e|tJ(}$QBBpNi&r=9K)?j`>904BbcB=^h zcCy1X)+5$x%}+$3!ba*7Vy#dRi&f{R2(G*!5Ju@1tev3QFLfsMbYSC}YuPSL`q-tn zxUdZ{pBA^L8uhzYcCvSJEE`OBZzv6T(#nSke>Xy^Q1y}|X}oR(xo_WC8+KTAdT#vuWT5={_n_{x5>9@5T zS_EQQTKM=l?`%)zG~c$?>uF6}AGgb1zLjJ<4HwidjS8M(KFVjW0rDPe+EQ$yjzKux zoMQ^s=Aa#<@y?j`r^n;t7e3F3f9~)0jqs1uEMazcuL#9of zEIzq#c^xgK{a)9rf#Cm8i$xz;?ztIV6|iJemW{1>j=Frf-8q{~-$s;%8!fwNHrr2b z8)cfSB5zm<@YL{~3#DI9R4yQ1H~HvZi^2S#@MUjfz<0soPaS;=zhn4mLWF-c*TuD_ zVH~`4PxnQ>B7hM3{#G#-a!tdjDQLOPm+i}u$uQ|%f#CAZah{RL4NfW`@DaWr%4Q6B ziH?c&BRJDib1~*!w&TR9)(Q+*sd)M$OpS8T!pkD7J|rzQgpjT3^zxTq??qNHL)Q`V z#i7*6WP|FiVBiIaZFIaR{ox#E3Fb#FQ~19CJ|5rYDwR}*JlC+RtKvIz*yX6&KUoLT znye0sI9epU7LVZ)?b%3=#CHm;k)3q)Pd?zu^aK2_lw;#EQlm*l#h&wUITf0(^J8p) z9NC0*u0%@}Y_)_g&2~(bjSHdrT9Hqk<#=A}E?6vB#z+EL1}smASa! z%a|&XJ#upr#t_e)>-KP%9zG+VevDfNe`E9d(YM!d7`rA9n!RmbL{58jwFvN!E;4fe zk7(vJUsWcsR_KVTia*E)FK`zf2H(Ng+{jAJdP+Wnzlcp_iJ8!$m>$3_6i z|It-PoT!Os#NzehD_gutBRUcIpTKB4K%0)yc`vbrmz$7)jkZO`QIJ++=y$(v&NDK? z#HBleD&%eN$zS!;Em@zw1H_b5W(O%u=Mzc_kfg~R^45ECc=4YeN{{szPbPc?BV6{x3J_5LMAR6FYHPdX6U!oP$_v? zt4?)uTAAD#Hi4Q++od)UsfBJDjQ*#gJA@iLf2uTDemswB%A?o5y~I3w#OUP-@a`8E zy7XW9w*%@?+cs8T@xmfEaw9Z@-K}4g+H9t>F)ni>7dJXkS04=3PGM`RlEEBq^Xz~} zZ1vMMufsgvw~o3E*Eex_pp^H&Z@2#9i?dW{6cX!`V+?fmy0dG_iZ-?+!HL7hf|{pdBO_{Vso-9vy$38 zV3ml$SejMWr=pC{^xspOW4dfmJ?6eZgovwOx5~yEc119ZIZxj|n+-!?ZnO#ZBX&*K z3~?>FdM}MfX>m{B96+J;96Up^ChEph^K}yaYku;<`fp2wZdFyt&$@EP%FW$t%Qb>=%hrAz>Y2g3D&taUHfoo{GS|zJp9T_`YzO92w*vC`bvJ+D~ zm6A}Jz`cv6GbW>#1TV{&yV_kM9RCg)+TE?_f3OUqkWc2k87`4M+lc@~%(-oM(x9m} zq8`PY69dK39ycIcbr(kG?eoAppCLEYNn`om^=@{LlxJY`VpYfM+-$S7t2Hsx4J7@+ zoV!acoKbP3QG#C*20OZyOJ^Vdr{476>FH@?WTbw;8qmsFLT`1Bi*IpVZ>;1)s1*AC z=yo@{aqU6kVa@C{RMTZdBS$C3hX8pnkA3kM`2Y_QuLj_aS%Cx_>pH8Cm0 zZcK7=O8?>QpVZY7792O{s}jR1BkB9Q28>nd)HEv>*{lo}*K_O&jjjM`WpO7p=TUE| zTi$9xuv(^(N0kcn$7|Jd<5MGyJF)mj>a-|T57W>$66z)~41(+%DY*4pBoVrdgq!3A z>K322ZCmzTRCH6x2c9|U`8Li@=jOG`l$#5PrT+oz*&D=lhe~*yaG|YWx8!*-i6Ck{ z%CIHH(Dsk4N7sZ8<28S6KQ9`xK$EFIb2;c^U;C`#41*Tp-*iE6qe5R)Y(6YV&36p( zD=SStl<401@d4<7J-7_kTUiUf!NkbU zb1iDdGAOiKB9&k7SL})S{lET;(C@gDS^fQ8Q;WcI_^bVjpU*k{`%7*elqOq!U5MUx z7_qp>Q2h1c>ZCeZto)WRuPoZHrGf@xRG^x6!LD7Me?~_Y#)nwhS-#%dLyLI!-@`2rvZ& zyIukk3$3r>Jvigo~9`8%LTDO9WfF z%&j&`i#35=oWD(4VmdTblR;dSBfgAzrgs?AT&tkPgjOjnAs(c!?T9b3G6Eo)xfmJY zGb8gQu!;Zjl}+&p6m)|`DKxzB1!3{XAL7>H)GLIw3o-$!44g=p+m=es6Evzgb+7q~ zts>u;);77(dI(Kw)Zs*p=;>Imq`C&E@jW3ep;c((4+gXOC zG;f@l=`@gbrmFI5a;`@OLcaH48ebb_E+tzDKUZ-kl+#{3ov~j##$LpMcP+3f< z8b(t1m2(3YJ$5Law~@AjthT)T&-7}yL|QcdrA) zpAstZ`jS@|)A41r+(pZjLh^`kw%PD^@_0JGosmS@jA3dsyqj-CM^W zC9X7CqI&;Gj}I-ZcMm!IT^xoh*rFrKLX!0tO!d95wUI9j`~4o3UWYLV{0fA2`eGL> zpo9%!jokVtOT6uc7!0Z%Ej!Thf!E@6w>vS)*IU8AftRGOSqo?DK}2(v#h7kXLuT&Dgr6`l_bOG*4&7)X)!GOa?*!kr_EHj zxUvK#Hca7rTo9@2ty~%ymX=;ieLJL0nRXam^_wTfG)3Out5yAzucAro?X{ zqJdON=QeC<{}f4Gn0#$RwP7>j=dwGLl&jAl{NCQ)uVb6ct3o*I_@qqGo85S}RQAuD z41;st{`3Iyx3Pu7QGY8Sq09UIEDzn3qPw}w%G>OGA6)035g~b1zw#m#-%|fU@5fcJ zy)C@K60&`(l1gJ-q}^Q9L3evx0-EHGzzm3e@lI8KXA9J(L)zkM7U$>I2YG2HeD7!<)_(F$YImE} zbfk2trF5FlJ661g4!e3yu7`Np!%IJ_d;KlE5^a067oXy{O0Ge_2IQku4hRVF_7-|s z{tdrGAdz7;gK`Nul1B)TDl<073^DZu)VjG%2=pHuiQeM7H`2b{#5`(Cb>K^nzrO70 zT)w1T$RG7TqvKe&uqr;Mg38XOx~j{hX}q;Rfj`^|IY?ZM^XM)PthBgdTyJ4IeJ}~i z$atTmU*7Nl>?CXXD4QP;@}sj_|kdz9=6n*CGcZuM*6b^TkEz zY?s&EEZL<>k|@|frXi%p!`i9Ly$KwmHEE!%p=h7ZP5lra#O#Dv<7R_=n((}Vk3WjG zd>Y&U6hGc!%7RsjvhZt^t02t~ctt?ADi%#rb1=!d9ZmgV*RK4}xI%X!vR0JWnoGlw z+Le8GB(P(kDqW1%Zzo5DHLvB@_i_8a=~^>8fe+*t6GOPorPs_v;Jq^hy=KWL&}a!PVyMnY?u@?JD_Qj0N}H*o-Q92>Jt z1Y!;Z*2+$ywTg%->pliLIk3A=B+Id4fIQ7h6bC0`KWS)=1@XM8F0oeEEWh=>+({;H z6lQ@A9XLIlhlm12wLo|O4$$da*9FYc<$AZL;^R9q+Gxw_wOJ%VU){;nM{6xU;kCL|pXG(+M@%gN@FCTqjvrW{SF38%NlN*3{4$ zD;dHMKggRWz(H20)gS$513Eb!wJTC68{2mN)yXRQfDE)_B6?*tb5>DbU?Ok@oTY z!z3}lCIYkdjF&%|!0T-GxF1lcY}|e(ZTe``5=Jgn$6Bsk{fl0LyIw=e2GE^FNGKqm zE-YWVw{@avdnR9V)K=FZ(0aV$Mv&enj5g8BS z;)Ne<4G#L-b8Vz}g($ctq_-^fTaEMu22ED9Q5~Hy*7PgOT040uahWLl_27Jc{OTkw zF!onC(!RY_Fx4eQ3F%iQb%OscXsMW~AXdWEm=u--$|me)7!F9zQxU2&M`nPbRu$s( z(Rj65y9BWfa)GxJGo@z`86q)zm~OBHnXJnpuEfXYDzskfOH^w|cE`&BqUZ`-BzmIs zXCC7OsQu~%sR=DxrxpF{cJ4e)^Wrx~*%D6wwccQ%(lxl&e8k5lE+!)$Sf+scy>e)O9{2N&bB)t#OGQ;So7JZd z`_)!C5*wtj+ZAGVFlC-AL&Rez7@=b+ir9W>`NIek_sT70(S#I45n3LEQV9bv&Gy;! zh+;ZX+b#oZb}vE0l1^OYJ0P41Zu_GT=D(J^KbUDd{ zNG;Ekd@+biQ;;}Yyz*DicJ?qPb`9Cq{PpsdGT@uFAdxjscsF_S<@LFyHC=nZiqjk3h3*o_t`gQJbW7YwCD2_JC_G2@c>UpS)jWUPf4)p8Gh6+ zW&3T}YJ-sa0M}9c+$e5HLMlFe*mfP_VxBl`LciW-y8RoCyR$!2mKitR7OcS3`P*IA zsQ2|*=NsWbV%_*gcsTmYRs2JvrwTdW4Nh~|C)PG>@+~)rvGY|>*D7d+HEQ^|Zwtwx zi4xY&*TKJ$c0IL9$=SQzF6sRqK1{nKy}BWcOn)x^`{qVA_8Z`}vq>+vfZSK7dY^h; zPx$_j(3!qVtsw8q!wuH6)g_KbYYC3#0`|~Q*5hUL!!DIym&NcZ1vFd^ehRVGe8#a_ zT%hDfG~jX^!4boX2dwCd3)*T8rko(>{C6B3rBAR*NrrF5%`A2uJV5u1y`RJUxl0C1 zvzCf3=NP^uLD!cR8Hui{BBg8rulc7Qg9Vy((dBU&h2L+gmPXEhgmPhp>aNa%Z=(3^ z1XM0jmba(R6)(VtlhEgn3r}FUtP9NLS+?8m>2fL?LCz-*W@1VkD4)7MsPtQTX>5FAo!#2x$Ft>ewqAZKAmc6L zUojyVys8UMc_YBNL;=I6WAp{Y-4}Cr+Atyjh*#9&!hH4b%UmPWPGGBu$FjAG@`*_1D0S`U$YvE6%g_GPwE#(fDgR3fQ^c9J(6zU6-r>Ip+D#{HoeN?S5rrG$Hj1!oSe*ls~MSGv&G5& zX#Ak8rd1mM(VRO+7L~N)PZgh4@G>f>LOR-Fut30g{oy7gq1Si}GsxWy=`R9Wza1Pl zGAyc`&k?N{6rZWxT{rB?L!c_`7A9_CtJLEi;E2jI`W{P})7hcFK`DUPi|chYJ+4qWU`;69_H;D7o1|E;{e90#tJqK4#@_d_ms3>-4#5iWwFt0Wa@fMa zndvSb6>Ig0nqCe#)s*YwqqAPSJ6!si;F!%bQ6%?W0RG8$an~UtKp%39HXlq)(yDS4 z`7V?ArdPzb+~eeDmO*_41n&!K2ZyZ9Vps=WOwe*2my)#-$OC@zUOYBnFsE!)Z|iG5 zhmzFCLBor(1KWVGDRk(mVUxw?;OhmyU4+VO$5wwcDAI*1ouPc`jI{Mp3B9- zz6`aRXJF;`%a!E~4n?A~)}%I{>*#UAWcRl|u-hUssbwOE6wEb|6(qtf)O{_p4;TuTcWxHOThYFnhAZRD~4F3DX&o*!?Z2aP2^KkPg za@<OBP^$}C|1kF4XC6VgAeN|3On)hQkHGfq(NcV}j zs6Afj>CtM)&Mt0tY#ij6WjI}udwF3wl8ojlltUcn`jE74xD)E?$wezw-WgrI5MQfq z{%G_Rw7*LHJfK$Mogv{M^EhJGA5U@Ob;BM5d*y%2gTdiE3x}-&PMgM zbFE2phR`2Ca(3wNP&(k)r;6=ncrRRe2uxBomN`tBq@od=IeB0w6_{Rs?%xg zZpVvszEju+-;DhE-kJ5yyCz8d`)2KdT(>A6As;>kQKHFlE8Y*V+x_GtC?E+@cGMPZ zkEbcZCQFxIdZ#^;D<>+EGn`wjRx~uZthbjSSC&Ros~y50FeA(vmPCFl)JvOVPz>1%qs*6PH3=Ku8G>&sZaICi zV(vItVL#f-5zXS2Ludj!1Dg_MGPQkijf>uf;nlbo>U z&G*MsWhI_Ki($P+{U}qJ!PYi5^9-!I)LX|q&6`#audZPrNy{oO0qCQJ`hLVxBeZCT zeZZPTOaH~ES*~q>RP##e?}SfDH4O|@n1<}KmVO|p)t*nH!xS0P zIqW~Mv!*uYcbCb9jp~C(Et@DZkNYm)D^oSRvNQpG(>p&(>=#29$>tRfs<7+CTUPK< zvW!TTCZFW1Q(SrGmxBwzYR2Nu_E!rNi(PmlCDj9i z?+~!S8(FnL4$Nf8;hM3pPbOP96f)|K0pzimZhy`eiaGwF)~eD#H`1uuY}zwQo5l)t z?}l3!mHdtgUWmtG%<<548|Jz;^(vygZ))@*sRtfqU>?%TE+5XRghN#I@CRL<1J7_4 z1r`Jn^+y2(Ok{wjQ$T`r`gha~hqwGAaiQs(f$vp`evqv}Uq1WyW9t8}|MLd4j0wZl zujxaXqhK&tA-J@ZQss{L@Zf-zi!0^R>ca*mL6t!ZA*pHTpQ)q6L#T)0w?yZeB3&mZ zCoq_$LH>sFpD@u=0Tdaj>GtX8Uw7JHaU_*Lj=DaHa(6@`A|kzX(eKlg!n4K+NIzw& zP=yDfuvWRv+Q9Awd~qmwRe|vb=3}6Qj|hNAwq$IfW(t(E}f`*5JhO}fMh4??( z5Ne?4p<(|u4hw0sLBl`+Ahjl>n1{msYup0T{^!a|gn~MN0R2Dpe?bZbxfO>91qXp- z1+ue3Xa9TFe=x#l|9hsNkoG?p3}jxIe?flZ<$<*R<0&>))_?9k$ex>l20fOcpt!wd z#6?xzbWbx-y}tL?zq?{lA#J|mam4iv%Eg6#tM|*k#FS;({p%NIt=cr->2dn%CH@i# zO(U+F|5~SJb^d+Xj1A_StQ_(aMi{1YHPV$d66H=eKa9VK-^Hl!SlOcE-D0Jq;)2XL z&GGRiE4!!3-ATsdX}w97<6KN}Ny!va021A21_p`GJXHOiaH8~)2|#8B!MfMymoK5i zmDZj5b7H^&#(h2^Fnj%&#P#g9*js)UWe2qdvY z&SA|)`@W#1{`9z?_BBh$@yckoaox-JEjC+B1x#VUZ0 zs<%1`u-RJzPzDo`3+vjSs7n;ZkVf1k;hr*`PF4#hNpVJ6thRiYkf`E5=(bo*r&&UO zpRoDZTtuO)?BFxV(qfPUH|h|&Hs2U)ZL^$zKiF`AuH{}W!sU;z2E|8P=jZy*J4&~z z>n%lCm;SuBZ9d;kW3`6=z;dE2E_W{#6^SrS9K?0mddqL4P1(duM1-x0l#ZC)iN zq|u#^`+-ibl)n%8wElcHH}?Ss=|f$p&7Y|WxPtyz{eWt&ZVnI*9Yq=eud*uIlH(y< z7hjwo>hRxB@U)?e$cw{pQL9jN+ON zR`z!%?(rqKo{ox&<44!NTxc+Ya<5izS$TiYH2f_n8oaZ<6OCopBbOP%E(scvR+X}T za1;Fj{@(HnWFbYiM13GgQ`DvCv?g@klf-sr{u3KgF@LcSa-GQtGS>eG@8 ztJxV9G#yDF!ma3NF}Ec)YdFZi{Hz7Dh>w-#X3g6EoRImW9j_mpwk6h?wQ1eL99Ax7 zc2tyBote+&)<^j)VZGNckH~HVC;gGh`V57z22N(tF%{vy?XIdV8O#bItl5n!##S_7 zS}1y^PwmO7sn&Fbe6-o&(vRC~!$>JkZr#BMB+eIIIXU&>svlJ)2?-mmi5>TDG^ABC zcs(z=;*V3MdY^x@e^+wYbNXdpa-!I}fVUxLNJPtlgprT*(M(x10tBLrcKuV8T8~Vi zI|@^U{5d661l|y@>a<*n$qqOhJlnU3|l6)WNN>Pa}j-A6cLcrrx{4%(CrDc0M-PZ36kv*5wmFa6R zaf|aY4Hq|GzW>KmoM>$@+!RjsIx)6*VxW;cVE7A=7HOA~5Ig89E(%%9T^bCRBS2#q zphT9yCQ6SnltS@SN`_D+-6DWBH$Uo=BBv;U@;5L}Q7T1L@y#?6fra~HxXmh`zfI>- zUA=859KC?khS{=9`Io1ht%%oWpT}9#xQEWy(9sTOdYZlSm$Uk|a<4oWmn$cED5Yy# zL|LJ>XCcc(%0=>X-m!p_HsAS_aPI!vC6dLRc!gs%QhI-^#lEHR`j~0yD0M9jIhr~2 zz(x3}s-UOoutI{OmR+fL25k4D&lGICBk8dt5ml3e8=NE60rkjpp>-=kAZ)iGhz?Vs zYj94Nq^fSHIh8UvW!+#7Ly!u}LMJj#x2W7w4U{4AHMErvJbgNTGmyWxT!zL@co|Og zUT!#_Z)ms1uIsA$g^y~4?IwfmEk}%(Jp!Qz-GDS4(~_ac2SU)Qt0;Y_81Rl(@%C(3 z#d`_q^wxU0VuK?xw!Zah=hX5?o#qodPkIwFRKaT$>O=#w!N1f2^g8j+y}!tQ&J_Cm zSae1V$VpwrUHb!AmZ6lV0ZK5&M%qR8e;R4uNz}qG0wk!_RT+co>KxQWV6)-05hAHt zX{#*Yml=TQ1jh?8eR!4=4bL?$1TV};-<2f6&O}9;Fon3@CJ=$XO7lKeQi55~1+YhS zFTu%Ese`5k>97@@cciVV8-Ph&1#oo{yAZTztrR$7goK{BMN0>hM5H>%s($zItqpI_ z3idU9CCEq!eQ|mk(B_8NP)_wbXV3Y6t@zny+5D-i4Xk(9V*8eIrkRoIA;@+6OU~vS zV12k?{eGC^kP&=efWxKmhlu3L65IzC6X<1HrS25?e;$q>j1Oyrwq#hw0yGTTm65ib z1SrLXr555B5t;D%_%H}x!4IU^Vt%YTgJ(C^-c3#QL$J|nh2dW0@@F)|ordCB27Ok5 z*bKI4xKzJ*ZjZq^wF{fi5DTJKh;A(xW*4}n>IszsJYM>np~-<9BE?l6xXT@0ULtD$ zhZq;cJMkC4JeBMpX8swvrTYI@BFy+yRjaab)S`AjzjDBY9Lc0xbUmdlOWi0z;`CfD*vPGt#e6ImNN8Ft+s(9_m&mK;HAFGT6_t>oRlMA?UZ9P!?|xc1`s2SvzzMt%>!3Nl!z$N@$of13GcZ(wl{d-wPodk5e)xVBHBMe6hqz28tEHi2|>YGkcz_e zf>pJ(<>w&fZa-uAW3@?kBHuE<9$F&2PddL@bEBdJqg2q&59o;mwX{H7c@9wbA=zwp zIieWFY=&Q;cG14Y_qZI5X|lUnEw{Luwc_4H-I32D;lOdrL){8pkFu+w!9qTNDbFmA z-jq(CD?Rk-rY0sV*v{@m$|3WZL(Z&IQdBh$=7y|EfCe%I8NT z;u6Y z3Y{v__q)^ec#iQTIifnR%92Pl?`yux(CBk3qcI*%OY(2gI!9txfE z1w%Gmv;{;Y{P6dd*JmMl$`fO`)|5}qIrJ*`V^5X}Zqb_N%Uj5;Ub$YQ`Q@^l;-x8U zF>WQ*C^Cg1&wS_m>(*xPxxBP=op*O(f8;xbMd^@bWjmM6%GdXYfp=a{E-zn?*Nwm- zw;tc8XkX z&GoMtC`@j$83~ECXK&BsBDV(V_7uXt+(`l#U4dD?Z@hRRls_u8;fbs4dmBe0hI7w5 z1Pcw*&V-2;ychgjR+s8!TAl;jdTcwEP;j$T%|z^E<>b0?5oKl7i?1n=DYiS`y=M($ z3kr)&qsWE3@8AnhE(ge;AGSLq5K#Vh`U+R|eigjkS*SKtQ6Uc;$yu139L;#Xie%|9 zYq1%gk0P8HOXJp4MH9}=rJ}a5=vZrU?D~nk)O-{$(b48~_fl{@3!i2Chu2qsGEaEo z^+*p>6HOHx4M_nO^#icOA2xgg&A~5GUD>|Icq9oCgN*+3=Q{6SKYun@&R4XxX>(lA zaWk<|o}$JhkK&snCjz~>DR_;4f0`3xY4Uo!LL~QsLdemmrH7IQM-i^Jx>g3G;_Jin zpGXq;X>wq`T=ZgO@bgQeo~*S$`@@%Um~pZ8dB!VK5SmX9#!NP4P=GrDgHE zw|Q6UBIlibUaooVYS6iKU0Z7~jXfGx`N&lHzIra)$>{h^Oo!~26Xvh=e8sCXi8R4ExE1?B6NwgoOm+kYmTx@Fg zxe1~g4!R#k$FTwJ&xivJ&MURdjRx=C2TA2vk}KX{-a9T`(Uu#F zZZdsVpPV8We|V^KO!Ge6B3O_1V#H1fJwL_K|G^Cpe{*EvHa*;tNtD=2P2qPPzd3w2 zPrx(EVP!h~7>^N?!S4!L4%A>D$v8hooF3ZZxU8($-G-nlAR+%< zj$AUcLgtIK#nJZE)Re;!zfuFGmK+%=G5-@~Z?jSIV;zT`mC1E8JXx*X``;aH26&dG z13<1Zh|}BI!IW;pWY576V?n>_<A3kZI^ywsvW#HMY6!DWx zMTgxd%7*7*j7;J8p(6LRPT>}B&+EBzO&2qKLR@>E%4`7^-}(7~P*vaxh=CzT0P}oQ_&^ls_-oq8> zv1e6R&pQOMT1ARkr%o#OqZ)}L>yL7!(2WunocN{VYFR?whw{b>jl{lgwu=ewu}*C{ zj=P7SuXQQ{^}GwCJ8FvxZ`&-R8a9tDuS%t86_S3id5fWJG~v2eEHlnFcG2rAnZYxRnkrV+KTF z+UA?^#yiBFJx2~lF4f^Qa z1Yb-t?_#5KRZzV4;O_OYq3Iw@&ojGyu$D7Zr>J zBoEHu*+FSFSbYQ{WV5F1^TNm37b#{aW(hF?NlG~@$^=#m27i7pee*f|TToUO`*so957}8Q z+4@|k?T!d{*loh7@0F+&MR*`Ap#7mk37&|xyrp~aajXN`_{0nk%8V<@N)O>Y0>WcW zzkcARO`p=~4(dlf-c>+Mb~IjvlapOT{yKj~BPA&=-e7tAbWEP{M<6-=&Mqy9X}{y< zpzJqBqs^MZ)QAcOX^DE>L1p}@Ar&h}PFy)uIVY(gM{JFo(GF61M4vsY>(*bXDbytuN4Vhko~=` z`|j+7@V%gB^BY@?`|O|Zy`dNb997QynKf;`&c^`9jmdBgN8gg~8KH-t8lX3*8lp0< z|2}(Td!Fz+7<5fy!am*Lr3IK=?~QE_#17O=FQOs_5E3seGif#Sz!S~DsbP0sJDx0D za#=4qoh~`AcWm*W5zMtXTb5bLK@kQT-rOzwdcD7nMxb2Edt+Cn&CKXQSVLi_7k1m9 zCqt@f>Gk}*1D2(UX4Pq0p`f4uo9^x9uqaDbK>k2Bd+n6u(&r9Ui{X!}#J%y(=;!fC3b0`B(` z^RfLgciOHZG8Yb7!rsS)mRtTwYsPKbn~I4Hel4|z&a$#H$9?9QQh^L8pc>A$f6%Gu zCpKyPC_}Cx0v-*G$HO8$GJcO-$w-*W#f!YYR3xs1ey&3@p*|MmG}of&0E9nJ^IM4yAn93k&M#JBE=<)XzU;@rD)WRYg z-A-cPA;tc3OM)O{P`l%UIX&j@D$##CMe=BN`Tao`IO$ecys*^N-q_q5ZPxT4RY~tz zQp%v2L{mUFW0GO89TY;X?Rp2qPhV^ACtXcQFyJYEczAfyxI_rV3Uun`d(&xkQTzVA zAADDn!8?zhj_3D|&W&}atERRDSzVM8I%KXcrgVNN_^5elMriTq3n4t6p<5~}imxReC#-9ZXR)BFXo=FP#v zXXdlQ12X{dnLLX96Co0X;Th+Ac7T6dThC^1Xq~lI+E*l`t)-GOfp+)nxwlvBJl<@h zJ!@|FkBA)~{UY_DPFT%T%+x_o-84ZZVUAd|MGKR45YO_aSah(83_(xRBBjDt{0np< zppZ&lIwgsfQ$HdCO@V-$&@nW1h@iq_cXvQvkdX@@9tfj?NlU{G?!XXA1NxVuK7Hl4v_o4IA`QZ#YUjP zDYL}+eYD9U?lo^2?S#|Q(RqE`Uw^skZA<;C2Uftu#AG)ehr2bHgHvac_Zzou-o-OS z1~CTRWWC+cNgMvCS-RNlrISyATD0E=m+Q6@$x>WylfM%*%33j`w7@?%+ii-eX-$Jh zN%>uZf=cxF$1|!ss=r*y=RJIB^t?OiriF^Zn-&)YTy~9|3;%u3^z@m@m54m9 zXhjzm#9L0K5!Eo%GB7mMH;IgbrVnz*z$WFOrsFfhG^DKZdG%ln<=}u59Av!e#Sj+3 zGYXHWF({)D)HQ6()?bKl&d-FlLfZBZad&qIGI42n_#BlKWdn{xD2+DhSXyT;o+obX+0tOOc zpY-I%mj3o_HLv9D?hv!|X!B_oF<`%V?>A3E*lZ&XDUgqH7Ei5mwcm{xWi%~O z7Z&ycAXt?@-6!3ev2qoosk|JN4l3xT*w!DGgB9gKnx`>DPSrCXP^v&uXHVZ%9|%{> zAR#9upt(xW?@>Qf9Uxs+v(t9-D>hY@1)zzZ8e6>tAqSz*qFUj|+emvo?k20FBTsV4 z;z*nsHDxn#{PYfay2N#fM6beR?7rlgwI7&4X-A;{Ie;YfgP-~8}uNwVUUw`0VOHE|W~f!D0z-v-=)R8zpA%8@ zZ-CK?K+EFV7hI|@05mvxv6KiZ>QBoQnRUO@YMp>WWwtvv9876~6uva?RIy<41Tzt? za0Lb2&ru3pMP=&z)pPn6rc$k->Carw?)u7-SSE}qd`<+-!IL)j-G{AqhcyfG05oXD zo_q4c_ZL0c8euQS%=N9M_qQEa50()Ec(Bdi>!qIEhZpe+9mfQ(SlYV@F$F7wjYtkP zLm!r8#T!1WAD?6hwKS&U>PVCqF&Bpug5U55Dosx__&H*`<&owd3PPo{FZoc|M1xTN zp+D|H=fzeJD{>p+>t6`6%txp@K9x=_(QsR9>MwEZ+Yp5CtyDaB2xfj@i=5 z_Ad!iCK7EqU+n{(US|fO-FIkkO`>4T$7YPM3potu8nDN-2mIE`2Z^d~7?BtaRYJ#G zK|vd;jnU1z`AE5<-6aoGo~5)g&05Rvk4t?;UVMkR!baxkoh7I?wMhbeY9p^=odeGj zJ$z8OAl zw~IXh_wda>^-o7vG)Of)w>zVFM@A~8w(c9hQi+Skj26S9Id-}xjI3Vq1w+nnGg*PX zk4fKk{=0BvrlyZgx+_o<@_jlE*54AOABDIJDTF@M{J-Oo|LZsy`3s@#`zk5sL z9D?(!sfX3d(1@?UOGW8?tF<0djW2O5?9kA?-V=|fOvTY1Brqc6za~rPHPNvtpG6u~%!| zZOwYc1g`If@Sc9HoEpYc3{Wr=%h36dHr!?7j+EQCzruI&bDOx~Cdj=PKV<@j1#)=v zPW3nJ7SuaZO?QNiVWi#U*_h;3N;d1GB$n%9H=!6`t3Oo4Ye=3_98D^Gw{gVi{f+44 z@j-x4&uVOD8HoMfgEB&Q?pdn-6Q%3HD~qT!8dQGw`bStbGyC=TeU;k@xX8CAWLD$q zWV%&FI8dx?sI>jhd01lt&G;H3TK&TB5fFBY5@0O__rAJ;i#enjn8)Lob7!gBY&x!; zn4a!}LJ)KL{H(T4TZxaju8A2-J-=bV_M>op3-<&)$*jfAH}ZH-YfF4MrsvU-8+_ME z_|5zI{-=-g+BjCUc^ca#u3*pbU9OmQpBv85jA^F*F&;sy_z$F;(Rbrju%cHOF1m-A zw)T*X{xMetN}Dc|cJmhKufUZ4`+OG1BRW@K!P}#=3)A-7V8^QZL9c$1opPj{H)d=+ z+Bdc1$5Gi1R~HSA!b^H92r)fwEZgm#5uR%frGqJp9WKuH6ZR@QPWVd;8Loi((}8hs zWBW^Q30l^__N28J+4O(wYTDXQ$Q#zzyb(n$P7=;lP|1yF#hM>?e5_Hzjuj)>2axXF z5*D}Zz;~eV!U#m5;>q$4hLunARv--(Jw+7o2oieH~=9r@!g4hfL2CLnSR93}4}Q79KQuy`I#f&zRmPx{bY6R_d<& zXlgytO@5sFskaN&bUL$dd;P->Jzy1rKLt#}m2KxEpp6qmiszciWI;3I--E!j`N~vr z(Or{$nkGU-OGF*50T#+4(H+I%cYlV<;evFnhL~)NG&)>G0ns5~Cc^+Ttp6_g;eRQc z|B@r2Pj854+x1kte1Xn=zMVQ??)w0@8DI)DndUBDpM-rey&(%qqdUUFJBu{1`QuLq zG(p(mwJwNS!t|rXw#lS8Li}npy47E9a@iQrlV^8LLa4B2vE0n|NP3UCvFnJv6LYh1 z0+y;HnC3dGt}M4H)JA}M2v|M7g^d|Lzj$l{&G(RCfPAi2D5!&o8 z?H<$^RUnItdnv3cdYcF92B+<-IL;b%{OZ^DRaDdxqd0`P7$|`De zhbU+egp(~MhTe{9L`-K$pkpi7V18NdUn19cJ%jZtr&2YP^NzzZJH9gn)%@tR;n>prx)FNOW1vMteQ~iTIs-$@%<(gYcN^k@A z`%oLkJoE#6S#m2#UDDY1;bakq>WwUtc=bOm70y(BWxkgnNp2OkjB?H+pyvhW-d;hi1!gc>#3 z2a>rV5g%-OvvJr9RQ2{8Z0{J|cxDk|M|$Z;_{XRMh_DV&*N|Q&`_$qKcv+80q6up4!IR8f5ZXvaDc{!9 znPQ-@LK${>xHYey*9&?P$_pD@h*<}9fLwYgtpuk}MSH1)gAVRaDtpgWOE1dPXcZ07 zxxaD%=~vpYZ@nRz6i5^QMNs~8GEq@d79{nkS1gN|n9}n9kbaa|6=y&9aQrm7LY8KdDXt`j>0b`i=P2wr7l5aj2*-s+EpM@6_&fTTA;Ot;4N z^s_berFL7BaYzGwzd{;CvpxeIXg3mnf2UOqJ>gR+v#vI0aDmG%{=hA97bgacx>`` zhzgC7>1NbJqmSBApVX>DTMo0JOu({d_)~{HfvX4yiqMnUckPvECe6CAn3$M|2#c-t ztq7>{h#x~Zuxk}UWvz<%L#q-eGy0uJlxBOFEvj&*Ym+Kg_dL7UQp2j|TM z-=yufN-<REJ0F;!5R41XS9|LCCBg=yk_ z(W>`*C>H>_ugcDk^|}A>6V090gqsqTG%+GvGL{>NM|7v<9YD)OPLHO|O2^AT`vc{4 zR18OvJ1o|;624;l<_6{)+tlEt7}!eU1FHJjgM+pe4T0Jw75m@k$KST9yf#w_Y(D(0 zL=<*8>M@Pgab08rjc@%#5R!J>yo>lop~j{Kk0g;-OSra9Gmh*6=laDHvz13un5uMy zH>koSMe~xX&$UwFrc%sTAUSLt$p0UCZvU;S{}%`M8;$ySpAfZ#QlwI>4KCa-zd>Q{ z?ApE&uEC`>ZZu+uD*X)&aps^2C{_`Ox1S>s4Dd2>sP=4fC(}y(VH|%LIvZ@}O8!`6{&%G`THJO#t!L&C{h6iCq+~6>v-e!!2Ds$Vl6Ut5IJ4lY*0rMMOh7{y05r z-=<+$adHYW$u=z9F=}&3ct_3N0e`!C6=*8}1}_R*2fk)CNYjnRQ2#iE2o}EM@0asx zQ$COmZ7<;=J!ziMwi}Y5vKdO0WiftG#AXC#-bP?AMeqCw1GH#yloT-Oc*^I{-|HKP zTb13I;yt0dp1Q`#nHM4U)|Okh;wM=Bc_4~l1s-Z^aXh3#^cu`e?S9;|St8v(oQ zbrOzFuLt+-fJl{*Od>k>l2C;v#SmrOmV|LRKl+^_~V}d7tpLiDNT02jvQ@ypExOP zz(HPTmPICX%kWA2q;R+jYxgAd=$$QtTJu0FL6T7LXZW^b1;D1nLP#3oFL-nua@CKy zY+{>x(Qa*A6?y?W_}JpxlE#G3bDMBe`N&#?Uo{HWzq$nF4q?Mdng65f5dO)J{6F)} z|4gI&7g%0O)GM{Y9K41;ZW&*jq2x|AtCFt& zz&+9Mn`+>>h5Oz}g>b=mt@jeiq z6wCYrRSKsr3C#!UG%6>o`W|e;^ORpT#j#}Lf`D66n>Cg>0n{#rz%l-u^LdlT5AKf_ z1Bdipt7r$?$zYzY4#fQr2m9BGNK`m&acqrBqLsTu5F;|q8PU%v>Ft+SCG|8zSx1Q^ zED*UqHPF;S&^=@-yfdMrFbe!IVkAtq|B5wq5c< zMykkq#-8tBgRaV&0QP(leqqFLqmTRU0Z6#9Hukd!g_SKR+_q69wo#lDXU-Z_x4{4t z-Oh=VNage1P5PCodo;nld*r8_8n{iw;553QK|N!{<|39}r$H^uv|-@zVW7$V9Q}%b zXi#O;CrAYs`4^&C03ZJIdjEIs?*Eou%SKqM4%ht}*Vy2O^Q-0zK~|p~88vfIJtT3f zNdfp+>nTY(4vyRN_=hcE|0KXkasV6hd@~1M7 z?>@+<$!_kQKI==aRDM23%$vBDZD@Z)@eaJbH9~HZ17r@V$3~l{c${%m*j>B48Wk~r z)yHLjH8?$-+RsTiwO{Vb3Z<525}`9`ZUCmuQX#=+4iXr4IKT)$iD2xlEcA?NO>ILd z%E;hV?-E`Wlf*33zP(hu7`!4aLSFy_E;j2`#tO@x1*8IJ>3hdQV^;0uvNTx?lv_Tj zV{VFlDUT*LZhD-oCTmo9abHg~KcX5WU6RAC0EVUQMW zH}qv8Vu8%2LDsAtm$qY`4IuheHIaARgF5|e z{FmJTi5ckk8Eqa$b^A2Ma5MpGnR=%)o3v35h_x!~uR(2zjhn z2V6in_-5V=EvG#+ULan%!DwRHwrG@SoiLQ6yEyz2WdoKM*C$_{oW)Dk1gh4 zYYMjjBsqaN#%m-z>M|(dcW|W^}w{5k=2K)LrUivCQ-L=eBd3t z9qz(cuXT(A@0n!rg^s}J7u+BRYpOJWc<*L#6x+;CzkZ+HZg<85Wg~Z7%)>w90qyfl zCrUV^d{YFM3nh2KTPRpIw8k&4c|&v2blfquPJt{6e=2kP8B?GA$kd(W$np^x__<7o z%c^%=4`*+&)X=7xdnv-6db9{l$23%}!sPL3Iv&o^F}EV|%TET%?vn`Zfk`hUf+q!w zjGE6-w*CY0rnefq0K*}Ch!r3ux?{E{LlB)I&m3MLC=i76x?Crv>Kh1GcM^arisCv4 zz^;87Zljq|zq<2GhWjA#72q5l3PfMbX%KxX_BUvI7W+XI{fRD2NFeg#Iq*Y3F?U-c z8!8rY%a>CrY0Rz57($i!tbl-{-d^=9bO@myee8tS#%!JuztPzva@OyVg~Zy}p`WtV zufz>|sUhL07Q7Jy-+)*Ei|`+l!ug5RrF%BJZj!r6`Wx?d2nqoR3Mq(w&Y{7vSqm$2 zg$TJB(*In=%xt5TK72B9H?iq&SR!D%LO?uSSe2-}LTY-qx}ksy8(X>dnK`z&_rIcC zSRZk*5=WM!_|J>9_Arm$_`oykFdc7exN!jGmRUAgVuvR(_+NZMaFJ*5vhR6_Ozf z4f(N4fgj%w9tx9^;_u8x zg+_3->i$q-acmUMkpvLWi8; img { + border-radius: 10px; + box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); + margin-top: 15px; +} + +.section p.warning { + border: 1px solid #eed936; + background: #fff7b8; + border-radius: 3px; + padding-inline-start: 90px; + padding-top: 4px; + padding-bottom: 4px; + position: relative; +} +.section p.warning::before { + content: "⚠️"; + font-size: 50px; + display: block; + position: absolute; + top: calc(50% - 0.5em); + left: 20px; + line-height: 1; +} + +.section p { + color: light-dark(#666, #ddd); +} +.section p.donatebutton { + text-align: center; + width: 100%; + margin-top: 3ex; +} +.section p.donatebutton .button { + padding: 20px; + border-radius: 30px; +} +.section p.donatebutton .button .icon-box { + background-color: light-dark(#fff, #27272a); + padding: 7px 5px 3px; + border-radius: 20px; +} + +.section form { + display: flex; + flex-direction: column; + width: 90%; + margin: 0 auto 1em; +} + +.section input[type="text"] { + padding: 10px; + margin-bottom: 1em; + border: 1px solid #ddd; + border-radius: 5px; +} +.section input[type="radio"] { + width: 16px; + height: 16px; +} + +.section input[type="submit"], +.section .button { + padding: 10px; + color: #fff; + background-color: #0a84ff; + border: none; + border-radius: 5px; + cursor: pointer; + text-decoration: none; + transition: + 0.3s background-color, + 0.3s box-shadow, + 0.3s margin; +} + +.section .button:hover { + box-shadow: inset 4px 2px 5px rgba(0, 0, 0, 0.3); +} + +.section .button img { + vertical-align: middle; +} + +.section .button:active { + box-shadow: inset 5px 2px 5px rgba(0, 0, 0, 0.3); +} diff --git a/pages/onboarding/onboarding.html b/pages/onboarding/onboarding.html new file mode 100644 index 00000000..8ab0c1d0 --- /dev/null +++ b/pages/onboarding/onboarding.html @@ -0,0 +1,86 @@ + + + + + Welcome to ThunderAI! + + + + + +

+
+

ThunderAI

+

+ Welcome to ThunderAI! +
This add-on is designed to enhance your email management with the power of AI. +
With ThunderAI, you can streamline your inbox, generate smart replies, summarize emails, and much more—all with the help of artificial intelligence. +

On this page, you'll find an overview of the key features available to help you make the most of ThunderAI. +
Get ready to experience a smarter, more efficient way to handle your emails! +

Let's see how to make best use of this add-on! +

+
+
+

Using Prompts

+ Prompts menu button +

+ On each message, there is a button that will allow you to open the prompts menu. +
Here you can choose the prompt in different ways: +

    +
  • Search box: Use this to search for part of the prompt.
  • +
  • Down/Up Arrow: Move through the prompts.
  • +
  • Press a number: Press a number on the kayboard to choose the corresponding prompt.
  • +
  • Enter: When you've picked the prompt you'd like, press enter to execute it.
  • +
+ In the ThunderAI options page you could find some option regarding this menu. +
The first two prompts are special ones, that are available only when using an API integration. They work directly without opening the AI Chat webpage. +

+
+
+

Defining your Custom Prompts

+ Managing Custom Prompts +

+ From the options page you can open the Custom Prompts page. +
From there you can add your prompts, and also disable the default ones. +
You can find more info here. +

+

+ While defining your own prompts you can also use data placeholders, that will be substituted with the corresponding information when using the prompt. +
You can find more info here. +

+

+ For example, you can use the placeholder {%mail_subject%} to refer to the subject of the email or {%tags_current_email%} to refer to the tags of the current email. +

+
+
+

Automatic features

+ Auto spam filter +

+ There are also two automatic features available when using an API integration, which can be activated from the options page: +

+

    +
  • Auto Spam Filter: automatically moves the email to the spam folder if it is detected as spam.
  • +
  • Auto Tagging: automatically assigns tags to the email.
  • +
+
For each of these features, there is an options page to specify the settings and the custom prompt to be used. +

+
+ +
+ + From b894f44c08f026fc7f0736bf0430e3e4c4c8b7cc Mon Sep 17 00:00:00 2001 From: Fjuro Date: Mon, 10 Mar 2025 17:17:03 +0100 Subject: [PATCH 13/92] Added translation using Weblate (Czech) --- _locales/cs/messages.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 _locales/cs/messages.json diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/_locales/cs/messages.json @@ -0,0 +1 @@ +{} From 59b279d0e391571ed052c0f4c4b850924076ff61 Mon Sep 17 00:00:00 2001 From: Fjuro Date: Mon, 10 Mar 2025 17:24:36 +0100 Subject: [PATCH 14/92] Translated using Weblate (Czech) Currently translated at 16.0% (47 of 292 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/cs/ --- _locales/cs/messages.json | 144 +++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 0967ef42..e268df59 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -1 +1,143 @@ -{} +{ + "customPrompts_saving_custom_prompts": { + "message": "Ukládání vlastních dotazů…" + }, + "prompt_rewrite_formal": { + "message": "Přepsat formálně" + }, + "customPrompts_reloading_menus": { + "message": "Načítání nabídek…" + }, + "customPrompts_form_label_Action": { + "message": "Akce" + }, + "customPrompts_btnCancel": { + "message": "Zrušit" + }, + "customPrompts_btnDelete_confirmText": { + "message": "Opravdu chcete odstranit tuto položku?" + }, + "customPrompts_form_label_ID": { + "message": "ID" + }, + "customPrompts_add_to_menu": { + "message": "Přidat do nabídky" + }, + "extensionDescription": { + "message": "Použijte ChatGPT, Google Gemini nebo Ollama pro vylepšení vašich e-mailů!" + }, + "menu_title": { + "message": "AI" + }, + "prompt_lang": { + "message": "Odpovědět v jazyce" + }, + "prompt_reply": { + "message": "Odpovědět na tento e-mail" + }, + "prompt_reply_advanced": { + "message": "Odpovědět na toto vlákno" + }, + "prompt_this": { + "message": "Zeptat se" + }, + "customPrompts_managePrompts_help": { + "message": "Další informace" + }, + "customPrompts_start_saving": { + "message": "Ukládání dotazů…" + }, + "customPrompts_reindexing_list": { + "message": "Indexování seznamu…" + }, + "customPrompts_filtering_prompts": { + "message": "Filtrování dotazů…" + }, + "customPrompts_saving_default_prompts": { + "message": "Ukládání výchozích dotazů…" + }, + "prompt_translate_this": { + "message": "Přeložit" + }, + "prompt_summarize_this": { + "message": "Shrnout" + }, + "customPrompts_managePrompts": { + "message": "Spravovat dotazy" + }, + "customPrompts_form_label_Name": { + "message": "Název" + }, + "customPrompts_form_required_fields": { + "message": "Požadovaná pole" + }, + "customPrompts_form_label_need_selected": { + "message": "Potřebný výběr textu" + }, + "customPrompts_saved": { + "message": "Dotazy uloženy!" + }, + "customPrompts_form_label_enabled": { + "message": "Povoleno" + }, + "customPrompts_btnSaveAll": { + "message": "Uložit vše" + }, + "customPrompts_do_reply": { + "message": "Odpovědět" + }, + "customPrompts_btnNew": { + "message": "Přidat nový" + }, + "customPrompts_close_button": { + "message": "Tlačítko pro zavření" + }, + "customPrompts_btnAddNewCommit": { + "message": "Přidat dotaz" + }, + "customPrompts_add_to_menu_always": { + "message": "Vždy" + }, + "prompt_selection_needed": { + "message": "Pro pokračování vyberte nějaký text!" + }, + "customPrompts_form_label_need_custom_text": { + "message": "Zeptat se na dodatečný text" + }, + "customPrompts_form_label_Text": { + "message": "Text dotazu" + }, + "customPrompts_add_to_menu_reading": { + "message": "Čtení e-mailu" + }, + "customPrompts_form_label_ID_rules": { + "message": "Musí být unikátní, být malými písmeny a bez mezer" + }, + "customPrompts_btnDelete": { + "message": "Odstranit" + }, + "customPrompts_form_label_need_signature": { + "message": "Vždy přidat podpis" + }, + "customPrompts_btnEdit": { + "message": "Upravit" + }, + "customPrompts_add_to_menu_composing": { + "message": "Psaní e-mailu" + }, + "prompt_rewrite_polite": { + "message": "Přepsat zdvořile" + }, + "prompt_classify": { + "message": "Zařadit" + }, + "customPrompts_btnOK": { + "message": "OK" + }, + "customPrompts_save_button": { + "message": "Uložit" + }, + "customPrompts_unsaved_changes": { + "message": "Máte neuložené změny!" + } +} From 943b3b9f237763822eeeae482501276875c99a6f Mon Sep 17 00:00:00 2001 From: Fjuro Date: Tue, 11 Mar 2025 14:58:26 +0100 Subject: [PATCH 15/92] Translated using Weblate (Czech) Currently translated at 100.0% (292 of 292 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/cs/ --- _locales/cs/messages.json | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index e268df59..9aba732a 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -139,5 +139,50 @@ }, "customPrompts_unsaved_changes": { "message": "Máte neuložené změny!" + }, + "customPrompts_managePrompts_info_default": { + "message": "Výchozí dotazy nelze upravovat. Pro vytvoření upravené verze je můžete zakázat, poté zkopírovat text dotazu a vložit jej do nového." + }, + "customPrompts_managePrompts_info_default_2": { + "message": "Můžete importovat a exportovat dotazy. Existující dotazy se stejným ID budou přepsány. Dotazy s novými ID budou přidány." + }, + "customPrompts_managePrompts_info_default_3": { + "message": "Pokud je všechno správně, po importu klikněte na tlačítko „Uložit vše“." + }, + "customPrompts_substitute_text": { + "message": "Nahrazující text" + }, + "chatgpt_win_working": { + "message": "Pracuji…" + }, + "chatgpt_win_job_completed": { + "message": "Dokončeno!" + }, + "chatgpt_win_get_answer": { + "message": "Použít vybranou odpověď" + }, + "chatgpt_btn_retry": { + "message": "Zkusit znovu" + }, + "chatgpt_sendbutton_not_found_error": { + "message": "Klikněte na tlačítko pro odeslání dotazu." + }, + "chatgpt_user_not_logged_in": { + "message": "Nejste přihlášeni do ChatGPT. Přihlaste se svými údaji, zavřete okno ChatGPT a poté opakujte akci. Následně již zůstanete přihlášeni." + }, + "chatgpt_win_model_warning": { + "message": "Nastavili jste možnost použití určitého modelu, ten se ale nejspíše nenačítá správně. Zkontrolujte prosím danou hodnotu a zkuste to znovu. Pro pokračování můžete stisknout modré tlačítko." + }, + "chatgpt_win_custom_text": { + "message": "Sem zadejte dodatečný text pro dotaz." + }, + "chatgpt_textarea_not_found_error": { + "message": "Zdá se, že stránka ChatGPT se načítá příliš dlouho. Pokud se načte, klikněte na tlačítko vpravo. Pokud problém přetrvává, zkontrolujte stav služby." + }, + "chatgpt_win_job_completed_select": { + "message": "Vyberte text, který chcete použít, a klikněte na tlačítko." + }, + "chatgpt_win_close": { + "message": "Zavřít" } } From 3e495db11e683c978c6d43272ba0ccf4be01ab75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Stan=C4=9Bk?= Date: Wed, 12 Mar 2025 10:20:31 +0100 Subject: [PATCH 16/92] Translated using Weblate (Czech) Currently translated at 100.0% (292 of 292 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/cs/ --- _locales/cs/messages.json | 690 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 690 insertions(+) diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 9aba732a..c769f5ec 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -184,5 +184,695 @@ }, "chatgpt_win_close": { "message": "Zavřít" + }, + "chatgpt_win_send": { + "message": "Odeslat" + }, + "chatgpt_use_gpt35": { + "message": "Použít GPT3.5" + }, + "chatgpt_force_completion": { + "message": "vynutit doplnění" + }, + "chatgpt_force_completion_title": { + "message": "Klikněte zde pro zobrazení tlačítka 'Použít poslední odpověď', pokud ChatGPT dokončil svou práci, ale tlačítko se nezobrazilo." + }, + "msg_prompt_too_long": { + "message": "Poskytnutý text je příliš dlouhý. Musíte ho zkrátit." + }, + "prefs_OptionText_release_notes": { + "message": "Poznámky k vydání" + }, + "prefs_status_page": { + "message": "stav služby" + }, + "prefs_OptionText_chatgpt_win_text": { + "message": "Rozměry okna ChatGPT" + }, + "prefs_OptionText_chatgpt_win_height": { + "message": "Výška" + }, + "prefs_OptionText_chatgpt_win_width": { + "message": "Šířka" + }, + "prefs_OptionText_default_sign_name": { + "message": "Přednastavený podpis" + }, + "prefs_OptionText_default_chatgpt_lang": { + "message": "Výchozí jazyk pro odpovědi" + }, + "prefs_OptionText_reply_all": { + "message": "Odpovědět všem" + }, + "prefs_OptionText_reply_sender": { + "message": "Odpovědět odesílateli" + }, + "prefs_OptionText_btnManagePrompts": { + "message": "Spravovat vaše příkazy" + }, + "prefsInfoTitle": { + "message": "Důležité informace" + }, + "prefs_OptionText_reply_type": { + "message": "Typ odpovědi" + }, + "prefsInfoDesc_2": { + "message": "Pro použití ChatGPT API potřebujete klíč OpenAI ChatGPT API a musíte vybrat model." + }, + "prefsInfoDesc_3": { + "message": "Pro použití integrace s Ollama je nutné nastavit místní server Ollama. Jakmile server běží, zadejte jeho adresu do určeného pole v aplikaci. Pro zajištění správné komunikace mezi ThunderAI a serverem Ollama nezapomeňte nastavit proměnnou OLLAMA_ORIGINS=moz-extension://*." + }, + "prefsInfoDesc_5": { + "message": "Pamatujte, že nabídku ThunderAI můžete otevřít pomocí klávesové zkratky CTRL+ALT+A." + }, + "prefsInfoDesc_6": { + "message": "Zkratku můžete změnit kliknutím na ikonu ozubeného kola v pravém horním rohu této stránky a výběrem možnosti „Spravovat zkratku rozšíření“." + }, + "prefsDonation_1": { + "message": "Líbí se vám tento doplněk?" + }, + "prefsDonation_2": { + "message": "Zvažte dar!" + }, + "backToOptionsText": { + "message": "Možnosti" + }, + "TranslateText": { + "message": "Chcete pomoci s překladem tohoto doplňku?" + }, + "customPrompts_ExportAll": { + "message": "Exportovat všechny příkazy" + }, + "customPrompts_Import": { + "message": "Importovat nové příkazy" + }, + "importPrompts_confirmText": { + "message": "Chystáte se importovat nové příkazy." + }, + "customPrompts_import_completed": { + "message": "Import vlastních příkazů dokončen! Pro uložení změn musíte kliknout na tlačítko 'Uložit vše'." + }, + "importPrompts_invalidPrompts": { + "message": "Soubor, který se snažíte importovat, neobsahuje žádné platné příkazy." + }, + "customprompts_form_label_define_response_lang": { + "message": "Určete jazyk odpovědi v příkazu" + }, + "prefs_Connection_type": { + "message": "Typ připojení" + }, + "prefs_Connection_type_ChatGPT_Web": { + "message": "Webové rozhraní ChatGPT" + }, + "prefs_Connection_type_ChatGPT_API": { + "message": "ChatGPT OpenAI API" + }, + "prefs_ChatGPT_API_Key": { + "message": "Klíč ChatGPT API" + }, + "Loading": { + "message": "Načítání..." + }, + "error": { + "message": "Chyba ThunderAI!" + }, + "chatgpt_empty_apikey": { + "message": "Nezadali jste klíč API pro ChatGPT API. Vložte jej prosím na stránce s možnostmi." + }, + "chatgpt_empty_model": { + "message": "Nevybrali jste model pro ChatGPT API. Vyberte jej prosím na stránce s možnostmi." + }, + "chagpt_api_connecting": { + "message": "Pokouším se připojit k OpenAI ChatGPT pomocí poskytnutého klíče API" + }, + "chagpt_api_send_button": { + "message": "Používám model" + }, + "prefs_OptionText_do_debug_info": { + "message": "Zapnout ladění" + }, + "Debug": { + "message": "Ladění" + }, + "prefs_Connection_type_OpenAI_Comp_API": { + "message": "API kompatibilní s OpenAI" + }, + "prefs_Connection_type_Ollama_API": { + "message": "Ollama API (Lokální LLM)" + }, + "Ollama_Models": { + "message": "Modely Ollama" + }, + "Ollama_Models_Fetch": { + "message": "Aktualizovat seznam modelů Ollama" + }, + "Ollama_Models_Error_fetching": { + "message": "Chyba při pokusu o načtení modelů Ollama" + }, + "API_Models_Error_NoModels": { + "message": "Nebyly nalezeny žádné modely" + }, + "ollama_empty_host": { + "message": "Nezadali jste adresu hostitele pro Ollama API. Prosím, vložte ji na stránce s nastavením." + }, + "prefs_API_Host": { + "message": "Adresa hostitele" + }, + "ollama_empty_model": { + "message": "Nevybrali jste model pro Ollama API. Prosím, vyberte jej na stránce s nastavením." + }, + "ollama_api_connecting": { + "message": "Pokouším se připojit k lokálnímu serveru Ollama pomocí hostitele" + }, + "andModel": { + "message": "a model" + }, + "error_connection_interrupted": { + "message": "Spojení se serverem bylo neočekávaně přerušeno" + }, + "chatgpt_api_request_failed": { + "message": "Požadavek na OpenAI ChatGPT API selhal" + }, + "WaitingServerReponse": { + "message": "Čekám na odpověď serveru" + }, + "prefs_API_Host_Info": { + "message": "Něco jako" + }, + "OpenAIComp_Models": { + "message": "odely kompatibilní s OpenAI API" + }, + "OpenAIComp_Models_Fetch": { + "message": "Aktualizovat seznam modelů kompatibilních s OpenAI API" + }, + "OpenAIComp_Models_Error_fetching": { + "message": "Chyba při pokusu o načtení modelů kompatibilních s OpenAI API" + }, + "OpenAIComp_empty_host": { + "message": "Nezadali jste adresu hostitele pro API kompatibilní s OpenAI. Prosím, vložte ji na stránce s nastavením." + }, + "OpenAIComp_empty_model": { + "message": "Nevybrali jste model pro API kompatibilní s OpenAI. Prosím, vyberte jej na stránce s nastavením." + }, + "OpenAIComp_api_connecting": { + "message": "Pokouším se připojit k lokálnímu serveru API kompatibilního s OpenAI pomocí hostitele" + }, + "OpenAIComp_api_request_failed": { + "message": "Požadavek na OpenAI Comp API selhal" + }, + "prefs_OpenAIComp_ChatName": { + "message": "Jméno chatu" + }, + "prefs_OpenAIComp_ChatName_Info": { + "message": "Toto je jméno, které bude použito v AI chatu." + }, + "OpenChatGPTTab_Info": { + "message": "V případě problémů s přihlášením v okně ThunderAI otevřete ChatGPT na nové kartě pomocí tlačítka vpravo, přihlaste se, poté kartu zavřete a pokračujte v používání ThunderAI." + }, + "placeholder_mail_text_body": { + "message": "Tělo e-mailu" + }, + "placeholder_mail_html_body": { + "message": "HTML tělo e-mailu" + }, + "placeholder_mail_subject": { + "message": "Předmět e-mailu" + }, + "placeholder_folder_name": { + "message": "Název složky" + }, + "placeholder_folder_path": { + "message": "Cesta ke složce" + }, + "placeholder_selected_text": { + "message": "Vybraný text" + }, + "prefs_OptionText_chatgpt_web_tempchat_info": { + "message": "Pokud je zaškrtnuto, bude ve webovém rozhraní ChatGPT použit dočasný chat." + }, + "chatgpt_btn_model": { + "message": "Použít aktuální model" + }, + "SendingPrompt": { + "message": "Odesílání výzvy..." + }, + "AddTags_prompt_text_title": { + "message": "Aktuální text výzvy" + }, + "AddTags_excl_list_title": { + "message": "Seznam vyloučení" + }, + "addtags_no_valid_tags": { + "message": "Po filtrování se seznamem vyloučení nebyly nalezeny žádné platné štítky." + }, + "thunderai_error_title": { + "message": "Chyba ThunderAI" + }, + "thunderai_warning_title": { + "message": "Varování ThunderAI" + }, + "GoogleGemini_Models_Error_fetching": { + "message": "Chyba při pokusu o načtení modelů Google Gemini" + }, + "google_gemini_api_request_failed": { + "message": "Požadavek Google Gemini API selhal" + }, + "prefs_OptionText_btnManagePrompts_infoline3": { + "message": "V zástupném symbolu dat {%tags_full_list%} můžete ve výzvě uvést seznam dostupných štítků. S vhodnou výzvou pak můžete vynutit, aby byly štítky vybrány pouze ze seznamu již existujících." + }, + "prefs_OptionText_get_calendar_event_Info": { + "message": "Pokud je zaškrtnuto, bude do nabídky přidána položka pro získání informací o události kalendáře z vybraného textu." + }, + "prefs_OptionText_add_tags_auto_force_existing_Info": { + "message": "Pokud je zaškrtnuto, AI bude nově přijatým e-mailům přidávat pouze existující štítky a nebude vytvářet nové." + }, + "prompt_spamfilter": { + "message": "Detekovat spamové e-maily" + }, + "placeholder_thunderai_def_sign": { + "message": "Výchozí podpis podle nastavení ThunderAI." + }, + "prefs_OptionText_spamfilter": { + "message": "Automatický filtr spamu" + }, + "prefs_OptionText_spamfilter_Info": { + "message": "Pokud je zaškrtnuto, ThunderAI automaticky přesune spamové e-maily do složky se spamem." + }, + "SpamFilter_PageTitle": { + "message": "Spravovat nastavení filtru spamu" + }, + "SpamFilter_info_default": { + "message": "Na této stránce můžete upravit výchozí výzvu používanou k detekci spamových e-mailů." + }, + "SpamFilter_prompt_text_title": { + "message": "Aktuální text přikazu" + }, + "prefs_OptionText_spamfilter_infoline": { + "message": "Příkaz můžete změnit, jak chcete, ale odpověď přijatá z AI musí být ve formátu JSON, jak je uvedeno ve výchozím příkazu!" + }, + "Moved_to_Spam": { + "message": "Přesunuto do Spamu" + }, + "Subject": { + "message": "Předmět" + }, + "From": { + "message": "Od" + }, + "SpamReport_Title": { + "message": "Hlášení spamového filtru" + }, + "spamfilter_threshold_zero": { + "message": "Práh spamu je nula! Označíte všechny e-maily jako spam!" + }, + "Spam_Value": { + "message": "Hodnota spamu" + }, + "spamfilter_not_moved": { + "message": "Ne" + }, + "prefsInfoDesc_4": { + "message": "Pro použití této integrace je nutné nastavit místní server kompatibilní s OpenAI API, jako je LM Studio. Jakmile server běží, zadejte jeho adresu do určeného pole v aplikaci. Pro zajištění správné komunikace mezi ThunderAI a místním serverem nezapomeňte správně nastavit nastavení CORS." + }, + "prefs_OptionText_add_tags_auto_only_inbox": { + "message": "Přidávat štítky pouze e-mailům v doručené poště" + }, + "prefs_OptionText_add_tags": { + "message": "Přidat štitky k e-mailům" + }, + "ollama_api_request_failed": { + "message": "Požadavek na Ollama API selhal" + }, + "prefs_OptionText_add_tags_first_uppercase": { + "message": "První písmeno velké" + }, + "TranslateLink": { + "message": "Zjistěte, jak na to!" + }, + "prefsInfoDesc_1": { + "message": "Může se stát, že se webové rozhraní ChatGPT změní způsobem, který naruší funkčnost doplňku. Zkontrolujte stránku „Stav služby“, která je odkazovaná ve spodní části této stránky. Také si pamatujte, že při prvním použití ThunderAI se musíte přihlásit do ChatGPT." + }, + "prefs_OptionText_btnManageSpamFilterInfo": { + "message": "Spravovat nastavení filtru spamu" + }, + "ChatGPT_Models": { + "message": "Modely ChatGPT" + }, + "spamfilter_no_reports": { + "message": "Zatím nebyly žádné zprávy zkontrolovány na spam. Zde naleznete seznam posledních 100 hlášení spamu pouze pro aktuální relaci." + }, + "SpamFilter_prompt_prefs_title": { + "message": "\"Možnosti filtru spamu\"" + }, + "spamfilter_threshold_too_low": { + "message": "Práh spamu je příliš nízký! Pravděpodobně označíte příliš mnoho e-mailů jako spam!" + }, + "importPrompts_invalidFile": { + "message": "Soubor, který se snažíte importovat, není platný soubor vlastních příkazů." + }, + "Explanation": { + "message": "Vysvětlení" + }, + "customPrompts_start_import": { + "message": "Spouštím import vlastních příkazů..." + }, + "currently_used_prompt": { + "message": "Aktuálně používaný název přikazu" + }, + "ChatGPT_Models_Fetch": { + "message": "Aktualizovat seznam modelů ChatGPT" + }, + "ChatGPT_Models_Error_fetching": { + "message": "Chyba při pokusu o načtení modelů ChatGPT" + }, + "Date": { + "message": "Datum" + }, + "spamfilter_moved": { + "message": "Ano" + }, + "Report_Date": { + "message": "Datum hlášení" + }, + "prefs_OptionText_spamfilter_threshold_Info": { + "message": "Pokud je hodnota vrácená umělou inteligencí nad tímto prahem, e-mail bude přesunut do složky se spamem." + }, + "prefs_OptionText_spamfilter_threshold": { + "message": "Práh spamu" + }, + "prefs_OptionText_add_tags_first_uppercase_Info": { + "message": "Pokud je zaškrtnuto, štítek bude nastaven na malá písmena s pouze prvním písmenem velkým." + }, + "AddTags_prompt_prefs_title": { + "message": "Možnosti přidávání štítků" + }, + "prefs_SurveyLinkText": { + "message": "Podělte se o svůj názor a pomozte vylepšit ThunderAI!" + }, + "prefs_SurveyLinkText2": { + "message": "Klikněte zde, zabere to jen minutu!" + }, + "prefs_OpenAIComp_ForceModel": { + "message": "Ručně vložit model" + }, + "OpenAIComp_force_model_ask": { + "message": "Zde vložte název modelu, který chcete použít." + }, + "prefs_OptionText_add_tags_force_lang": { + "message": "Vynutit jazyk" + }, + "prefs_OptionText_add_tags_force_lang_Info": { + "message": "Pokud je zaškrtnuto, jazyk štítků bude vynucen tak, aby odpovídal jazyku definovanému na stránce možností ThunderAI, pokud je zadán." + }, + "prompt_add_tags_force_lang": { + "message": "Štítky musí být napsány v" + }, + "prefs_Connection_type_Google_Gemini_API": { + "message": "Google Gemini API" + }, + "prefs_GoogleGemini_API_Key": { + "message": "API klíč" + }, + "GoogleGemini_Models": { + "message": "Modely Google Gemini API" + }, + "GoogleGemini_Models_Fetch": { + "message": "Aktualizovat seznam modelů Google Gemini" + }, + "google_gemini_api_connecting": { + "message": "Probíhá pokus o připojení ke Google Gemini pomocí zadaného API klíče" + }, + "google_gemini_empty_apikey": { + "message": "Nezadali jste API klíč pro Google Gemini API. Vložte jej na stránce možností." + }, + "google_gemini_empty_model": { + "message": "Nevybrali jste model pro Google Gemini API. Vyberte jej na stránce možností." + }, + "GoogleGemini_SystemInstruction": { + "message": "Systémová instrukce" + }, + "GoogleGemini_SystemInstruction_Info": { + "message": "Když nastavíte systémovou instrukci, poskytnete modelu další kontext pro pochopení úkolu, poskytnutí přizpůsobenějších odpovědí a dodržování specifických pokynů nad rámec odeslané výzvy." + }, + "ChatGPT_Developer_Messages": { + "message": "Zprávy pro vývojáře" + }, + "AddTags_excl_list_infoline": { + "message": "Toto je seznam štítků, které není povoleno přidávat do e-mailů." + }, + "save": { + "message": "Uložit" + }, + "addtags_info_additional_statements": { + "message": "Toto prohlášení bude přidáno na konec výzvy:" + }, + "reset_default": { + "message": "Obnovit výchozí" + }, + "addtags_excl_list_infoline2": { + "message": "Přidejte slovo na řádek nebo oddělené čárkou." + }, + "addtags_dialog_title": { + "message": "Přidat štítky do e-mailu" + }, + "addtags_exclude_tag": { + "message": "Vyloučit štítek" + }, + "addtags_no_tags_received": { + "message": "Z AI nebyly přijaty žádné štítky." + }, + "ChatGPT_Developer_Messages_Info": { + "message": "Když nastavíte zprávy pro vývojáře, poskytnete modelu další kontext pro pochopení úkolu, poskytnutí přizpůsobenějších odpovědí a dodržování specifických pokynů nad rámec odeslané výzvy." + }, + "placeholder_mail_typed_text": { + "message": "Text napsaný před citovaným tělem e-mailu" + }, + "prompt_get_calendar_event": { + "message": "Přidat novou událost do kalendáře" + }, + "prefs_OptionText_btnManagePrompts_infoline2": { + "message": "Výzvu můžete změnit, jak chcete, ale odpověď obdržená od AI musí být seznam štítků oddělených čárkou!" + }, + "prefs_OptionText_add_tags_auto_only_inbox_Info": { + "message": "Pokud je zaškrtnuto, AI bude přidávat štítky pouze e-mailům přijatým ve složce Doručená pošta." + }, + "thunderai_def_lang": { + "message": "Výchozí jazyk podle nastavení ThunderAI." + }, + "StorageSpace": { + "message": "Celkový obsazený úložný prostor" + }, + "SearchPrompt": { + "message": "Hledat výzvy" + }, + "prefs_OptionText_dynamic_menu_force_enter": { + "message": "Menu: okamžité odeslání výzvy" + }, + "prefs_OptionText_dynamic_menu_force_enter_info": { + "message": "Pokud je zaškrtnuto, použití klávesové zkratky CTRL+ALT+A automaticky odešle zvýrazněnou výzvu z menu. Jinak se uživateli zobrazí název výzvy a pro její odeslání bude nutné znovu stisknout klávesu Enter." + }, + "prefs_OptionText_dynamic_menu_order_alphabet": { + "message": "Menu: seřadit abecedně" + }, + "prefs_OptionText_dynamic_menu_order_alphabet_info": { + "message": "Pokud je zaškrtnuto, výzvy v menu budou seřazeny abecedně." + }, + "prefs_OptionText_chatgpt_win_dims_info": { + "message": "Nastavte na 0, pokud nechcete specifikovat velikost okna." + }, + "prefs_OpenAIComp_API_Key": { + "message": "OpenAI Comp API klíč" + }, + "Optional": { + "message": "Volitelné" + }, + "OpenChatGPTTab": { + "message": "Otevřít kartu ChatGPT" + }, + "placeholder_additional_text": { + "message": "Dodatečný text" + }, + "placeholder_junk_score": { + "message": "Skóre nevyžádané pošty" + }, + "placeholder_recipients": { + "message": "Příjemci" + }, + "placeholder_cc_list": { + "message": "Seznam kopií" + }, + "placeholder_author": { + "message": "Autor" + }, + "placeholder_account_email_address": { + "message": "E-mailová adresa účtu" + }, + "prefs_OptionText_placeholders_use_default_value": { + "message": "Zástupné symboly: použít výchozí hodnotu" + }, + "prefs_OptionText_placeholders_use_default_value_info": { + "message": "Pokud je zaškrtnuto, zástupné symboly budou vyplněny výchozími hodnotami, pokud není zadána žádná hodnota. Jinak zůstanou zástupné symboly na svém místě." + }, + "prefs_OptionText_max_prompt_length": { + "message": "Maximální délka výzvy" + }, + "prefs_OptionText_max_prompt_length_Info": { + "message": "Toto je maximální počet znaků, které lze použít ve výzvě. V opačném případě se zobrazí chybová zpráva. Tato hodnota není upravitelná pro webové rozhraní ChatGPT." + }, + "prefs_OptionText_chatgpt_web_model": { + "message": "Webový model ChatGPT" + }, + "prefs_OptionText_chatgpt_web_model_info": { + "message": "Toto je model, který bude vynucen pro webové rozhraní ChatGPT. Pokud není zadán žádný model nebo je zadán nesprávný, výchozí model bude nastaven ChatGPT na webové stránce." + }, + "prefs_OptionText_chatgpt_web_tempchat": { + "message": "Dočasný chat ChatGPT Web" + }, + "AllowedValues": { + "message": "Povolené hodnoty" + }, + "prefs_OptionText_btnManagePrompts_infoline": { + "message": "Můžete použít další zástupné symboly dat." + }, + "prefs_OptionText_openai_comp_use_v1": { + "message": "Zachovat kompatibilitu \"v1\"" + }, + "prefs_OptionText_openai_comp_use_v1_info": { + "message": "Pokud je zaškrtnuto, segment \"v1\" v cestě volání API bude zachován, například \"http://localhost:1234/v1/chat/completions\"." + }, + "prefs_OptionText_openai_comp_info_remote": { + "message": "Zde můžete také vložit adresu vzdáleného serveru." + }, + "prefs_OptionText_owl_warning": { + "message": "Zdá se, že alespoň jeden z vašich účtů používá doplněk Owl for Exchange. Mezi Thunderbirdem a Owlem je známý problém, který se v současné době řeší. V tuto chvíli můžete používat ThunderAI při psaní e-mailů, ale ne při jejich čtení." + }, + "prefs_OptionText_chatgpt_web_model_tooltip": { + "message": "Kliknutím na hodnotu ji nastavíte." + }, + "prompt_reply_full_text": { + "message": "Odpovězte na následující e-mail. Odpovězte pouze potřebným textem a bez dalších komentářů nebo jiného textu." + }, + "prompt_reply_additional_text": { + "message": "Nepřidávejte řádek předmětu do odpovědi." + }, + "reply_same_lang": { + "message": "Odpovězte ve stejném jazyce." + }, + "sign_msg_as": { + "message": "Podepište zprávu jako" + }, + "prompt_reply_advanced_full_text": { + "message": "Odpovězte na následující e-mail \"{%selected_text%}\", s ohledem na to, že se jedná o celé vlákno e-mailů \"{%mail_html_body%}\". Odpovězte pouze potřebným textem a bez dalších komentářů nebo jiného textu." + }, + "prompt_rewrite_full_text": { + "message": "Přepište následující text, aby byl zdvořilejší. Odpovězte pouze přepsaným textem a bez dalších komentářů nebo jiného textu." + }, + "prompt_rewrite_formal_full_text": { + "message": "Přepište následující text, aby byl formálnější. Odpovězte pouze přepsaným textem a bez dalších komentářů nebo jiného textu." + }, + "prompt_classify_full_text": { + "message": "Klasifikujte následující text z hlediska zdvořilosti, vřelosti, formálnosti, asertivity, urážlivosti a uveďte procento pro každou kategorii. Odpovězte pouze kategorií a skóre bez dalších komentářů nebo jiného textu." + }, + "prompt_summarize_this_full_text": { + "message": "Shrňte následující e-mail do seznamu s odrážkami." + }, + "prompt_translate_this_full_text": { + "message": "Přeložte následující e-mail do" + }, + "prompt_this_full_text": { + "message": "Odpovězte pouze potřebným textem a bez dalších komentářů nebo jiného textu." + }, + "prompt_add_tags": { + "message": "Přidat štitky k tomuto e-mailu" + }, + "prefs_OptionText_add_tags_Info": { + "message": "Pokud je zaškrtnuto, do menu bude přidána položka pro aplikaci štítků na e-maily." + }, + "placeholder_tags_current_email": { + "message": "Štitky e-mailu" + }, + "placeholder_tags_full_list": { + "message": "Existující štitky" + }, + "prefs_OptionText_add_tags_hide_exclusions": { + "message": "Skrýt vyloučené štitky" + }, + "prefs_OptionText_add_tags_hide_exclusions_Info": { + "message": "Pokud je zaškrtnuto, štitky, které jsou přítomny v seznamu vyloučení, budou skryty v potvrzovacím dialogu." + }, + "prompt_add_tags_full_text": { + "message": "Analyzuj následující text e-mailu a vygeneruj seznam štítků oddělených čárkou, které shrnují jeho obsah. Použij témata, klíčová témata a relevantní deskriptory jako štitky. Zajisti, aby štitky byly stručné a relevantní k obsahu e-mailu.\nText e-mailu: {%mail_text_body%}\nPro kontext zvaž následující podrobnosti:\n- Odesílatel: {%author%}\n- Příjemci: {%recipients%}\n- Seznam kopií: {%cc_list%}\n- Předmět e-mailu: {%mail_subject%}\nZalož své štitky na textu a kontextu e-mailu, ignoruj zbytečné informace nebo triviální detaily. Výstup by měl být pouze seznam štítků oddělených čárkou bez jakéhokoli dalšího komentáře nebo textu." + }, + "prefs_OptionText_add_tags_maxnum": { + "message": "Maximální počet štítků" + }, + "prompt_add_tags_maxnum": { + "message": "Omezit počet štítků na" + }, + "prefs_OptionText_add_tags_maxnum_Info": { + "message": "Maximální počet štítků navržených AI. Nastavte na 0, pokud nechcete omezit počet štítků." + }, + "prefs_OptionText_btnManageTagsInfo": { + "message": "Spravovat nastavení štítků" + }, + "AddTags_info_default": { + "message": "Na této stránce můžete upravit výchozí výzvu použitou k přidání štítků k e-mailům a spravovat seznam vyloučení." + }, + "AddTags_PageTitle": { + "message": "Spravovat nastavení štítků" + }, + "prompt_get_calendar_event_full_text": { + "message": "Z následujícího textu extrahujte všechny relevantní podrobnosti potřebné k vygenerování události kalendáře. Extrahované informace by měly zahrnovat:\n- Název události\n- Datum a čas zahájení (včetně časového pásma, pokud je uvedeno)\n- Datum a čas ukončení (včetně časového pásma, pokud je uvedeno)\n- Celý den (pokud je zmíněn)\nZajistěte, aby byla data formátována jasně a konzistentně, aby je bylo možné přímo použít k vytvoření události kalendáře.\nPokud existují relativní časové odkazy, vezměte v úvahu, že datum a čas e-mailu jsou \"{%mail_datetime%}\". Vypočítejte datum a čas zahájení na základě tohoto odkazu. Pokud jsou vypočítané datum a čas zahájení dřívější než \"{%current_datetime%}\", přepočtěte datum a čas zahájení pomocí \"{%current_datetime%}\" jako základu.\nPokud není zadána doba trvání, nastavte ji na jednu hodinu.\nPokud nejste schopni získat jednu nebo více požadovaných informací, odpovězte prázdným řetězcem.\nVygenerujte odpověď pouze ve formátu JSON. Nezahrnujte žádný další text ani vysvětlení; uveďte pouze JSON. Zde je formát, který se má použít:\n{\n\"startDate\": \"RRRRMMDDTHHMMSS\",\n\"endDate\": \"RRRRMMDDTHHMMSS\",\n\"summary\": \"Zde je souhrn události kalendáře\",\n\"forceAllDay\": false\n}\nZde je text:\"{%selected_text%}\"" + }, + "prefs_OptionText_get_calendar_event": { + "message": "Přidat novou událost do kalendáře z vybraného textu" + }, + "prefs_OptionText_btnManageCalendarEventInfo": { + "message": "Spravovat nastavení událostí kalendáře" + }, + "GetCalendarEvent_PageTitle": { + "message": "Spravovat nastavení událostí kalendáře" + }, + "GetCalendarEvent_info_default": { + "message": "Na této stránce můžete upravit výchozí výzvu použitou k získání události kalendáře z vybraného textu." + }, + "GetCalendarEvent_prompt_text_title": { + "message": "Aktuální text výzvy" + }, + "prefs_OptionText_GetCalendarEvent_infoline2": { + "message": "Výzvu můžete změnit, jak si přejete, ale odpověď přijatá od AI musí být ve formátu JSON, jak je uvedeno ve výchozí výzvě!" + }, + "prefs_OptionText_get_calendar_event_Sparks_not_present": { + "message": "Chcete-li používat funkci události kalendáře, nainstalujte si doplněk ThunderAI Sparks." + }, + "prefs_OptionText_download_now": { + "message": "Stáhnout ThunderAI Sparks nyní!" + }, + "placeholder_mail_datetime": { + "message": "Datum a čas e-mailu" + }, + "placeholder_current_datetime": { + "message": "Aktuální datum a čas" + }, + "calendar_getting_data_error": { + "message": "Chyba při získávání dat události kalendáře" + }, + "calendar_opening_dialog_error": { + "message": "Chyba při otevírání dialogu události kalendáře" + }, + "sparks_not_installed": { + "message": "ThunderAI Sparks není nainstalován!" + }, + "prefs_OptionText_add_tags_auto": { + "message": "Přidávat štítky automaticky" + }, + "prefs_OptionText_add_tags_auto_Info": { + "message": "Pokud je zaškrtnuto, AI automaticky přidá štítky k nově přijatým e-mailům." + }, + "prefs_OptionText_add_tags_auto_force_existing": { + "message": "Vynutit existující štítky při automatickém označování" + }, + "prompt_spamfilter_full_text": { + "message": "Analyzuj následující e-mail a urči, zda se jedná o spam či nikoli. Zvaž faktory, jako jsou podezřelá klíčová slova, nadměrné propagační výrazy, zavádějící řádky předmětu, žádosti o osobní informace a neobvyklé adresy odesílatele.\nZadej hodnotu od 0 (není spam) do 100 (spam) a vysvětlení o maximálně 10 slovech. \nVygeneruj odpověď pouze ve formátu JSON. Neuváděj žádný další text nebo vysvětlení; uveď pouze JSON. Zde je formát, který se má použít:\n{\n\"spamValue\": ,\n\"explanation\": \"Stručné vysvětlení vašeho zdůvodnění\",\n}\nZde jsou informace o e-mailu:\nOdesílatel: \"{%author%}\"\nPředmět: \"{%mail_subject%}\"\nText HTML: \"{%mail_html_body%}\"" } } From 0d6f00e99bbf1ddeb37d1f09a861c6fe36c1a6dd Mon Sep 17 00:00:00 2001 From: 67 Date: Fri, 14 Mar 2025 07:46:45 +0100 Subject: [PATCH 17/92] Added translation using Weblate (Chinese (Simplified Han script)) --- _locales/zh_Hans/messages.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 _locales/zh_Hans/messages.json diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/_locales/zh_Hans/messages.json @@ -0,0 +1 @@ +{} From bcd37f17bdc6bfb39351af0cada041bc6b4d6481 Mon Sep 17 00:00:00 2001 From: 67 Date: Fri, 14 Mar 2025 07:53:13 +0100 Subject: [PATCH 18/92] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 98.6% (288 of 292 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/zh_Hans/ --- _locales/zh_Hans/messages.json | 867 ++++++++++++++++++++++++++++++++- 1 file changed, 866 insertions(+), 1 deletion(-) diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index 0967ef42..9c1a443f 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -1 +1,866 @@ -{} +{ + "prompt_summarize_this": { + "message": "总结一下这个" + }, + "prompt_reply": { + "message": "回复此电子邮件" + }, + "customPrompts_btnOK": { + "message": "OK" + }, + "customPrompts_close_button": { + "message": "关闭按钮" + }, + "prompt_reply_advanced": { + "message": "回复此主题" + }, + "customPrompts_form_label_Text": { + "message": "提示文字" + }, + "customPrompts_filtering_prompts": { + "message": "过滤提示词..." + }, + "customPrompts_btnAddNewCommit": { + "message": "添加提示词" + }, + "customPrompts_reloading_menus": { + "message": "正在重新加载菜单..." + }, + "customPrompts_form_label_ID": { + "message": "ID" + }, + "customPrompts_form_label_need_selected": { + "message": "需要选择文本" + }, + "customPrompts_form_label_Action": { + "message": "操作" + }, + "customPrompts_form_label_need_signature": { + "message": "始终添加签名" + }, + "customPrompts_btnNew": { + "message": "新增" + }, + "chatgpt_win_job_completed": { + "message": "完成了!" + }, + "customPrompts_btnSaveAll": { + "message": "保存所有变动" + }, + "extensionDescription": { + "message": "使用ChatGPT、Google Gemini 或Ollama 来增强你的邮件!" + }, + "menu_title": { + "message": "AI" + }, + "prompt_translate_this": { + "message": "翻译这个" + }, + "prompt_this": { + "message": "提示这个" + }, + "prompt_selection_needed": { + "message": "要继续,请先选择一些文本!" + }, + "customPrompts_managePrompts": { + "message": "管理提示词" + }, + "customPrompts_managePrompts_help": { + "message": "更多信息" + }, + "customPrompts_managePrompts_info_default": { + "message": "默认的提示词无法直接编辑。你可以先禁用它们,然后将提示文本复制并粘贴到新提示中,从而创建修改后的版本。" + }, + "customPrompts_managePrompts_info_default_2": { + "message": "你可以导入和导出提示。具有相同ID的现有提示将会被覆盖。具有新ID的提示会被添加。" + }, + "customPrompts_btnDelete": { + "message": "删除" + }, + "customPrompts_btnDelete_confirmText": { + "message": "您确实要删除此项吗?" + }, + "backToOptionsText": { + "message": "选项" + }, + "prefs_OptionText_placeholders_use_default_value_info": { + "message": "如果选中,则当未提供任何值时,占位符将用默认值填充。否则,占位符将保留在原处。" + }, + "prefs_OptionText_max_prompt_length": { + "message": "最大提示词长度" + }, + "prefs_OptionText_max_prompt_length_Info": { + "message": "这是提示中可以使用的最大字符数。否则,将显示错误消息。该值在 ChatGPT Web 界面中不可编辑。" + }, + "prefs_OptionText_chatgpt_web_model": { + "message": "ChatGPT Web 模型" + }, + "prefs_OptionText_chatgpt_web_model_info": { + "message": "这是将为 ChatGPT Web 界面强制使用的模型。如果未指定或提供了不正确的模型,ChatGPT 将在网页上设置默认模型。" + }, + "prefs_OptionText_chatgpt_web_tempchat": { + "message": "ChatGPT Web 临时聊天" + }, + "prefs_OptionText_chatgpt_web_tempchat_info": { + "message": "如果勾选此项,将在 ChatGPT Web 界面中使用临时聊天功能。" + }, + "chatgpt_btn_model": { + "message": "使用当前模型" + }, + "SendingPrompt": { + "message": "正在发送提示词..." + }, + "AllowedValues": { + "message": "允许的值" + }, + "prefs_OptionText_btnManagePrompts_infoline": { + "message": "您可以使用额外的数据占位符。" + }, + "prefs_OptionText_btnManagePrompts_infoline2": { + "message": "您可以根据需要更改提示,但从 AI 收到的响应必须是以逗号分隔的标签列表!" + }, + "prefs_OptionText_openai_comp_use_v1": { + "message": "保持“v1”兼容性" + }, + "prompt_reply_full_text": { + "message": "请提供您希望回复的邮件内容,我将根据要求仅提供所需的回复文本,不添加任何额外评论或其他文字。" + }, + "prompt_reply_additional_text": { + "message": "不要在回复中添加主题行。" + }, + "addtags_exclude_tag": { + "message": "排除标签" + }, + "spamfilter_no_reports": { + "message": "尚未筛选出垃圾邮件。此处您只能找到当前会话的最近 100 条垃圾邮件报告的列表。" + }, + "SpamReport_Title": { + "message": "垃圾邮件过滤报告" + }, + "spamfilter_not_moved": { + "message": "否" + }, + "spamfilter_moved": { + "message": "是" + }, + "prompt_rewrite_polite": { + "message": "礼貌地重写" + }, + "prompt_classify": { + "message": "分类" + }, + "prompt_rewrite_formal": { + "message": "正式地重写" + }, + "customPrompts_form_label_ID_rules": { + "message": "必须是唯一的、小写且没有空格" + }, + "customPrompts_saving_default_prompts": { + "message": "正在保存默认提示词..." + }, + "customPrompts_reindexing_list": { + "message": "重新索引列表..." + }, + "customPrompts_form_label_enabled": { + "message": "启用" + }, + "customPrompts_start_saving": { + "message": "正在保存提示词..." + }, + "customPrompts_managePrompts_info_default_3": { + "message": "如果一切正确,在导入完成后点击“保存所有”按钮。" + }, + "customPrompts_btnEdit": { + "message": "编辑" + }, + "customPrompts_saving_custom_prompts": { + "message": "正在保存自定义提示词..." + }, + "customPrompts_saved": { + "message": "提示词已保存!" + }, + "customPrompts_unsaved_changes": { + "message": "有未保存的更改!" + }, + "customPrompts_add_to_menu_always": { + "message": "始终" + }, + "customPrompts_form_label_Name": { + "message": "名称" + }, + "customPrompts_form_label_need_custom_text": { + "message": "要求补充文字" + }, + "customPrompts_form_required_fields": { + "message": "必填字段" + }, + "customPrompts_btnCancel": { + "message": "取消" + }, + "customPrompts_save_button": { + "message": "保存" + }, + "customPrompts_add_to_menu": { + "message": "添加到菜单" + }, + "customPrompts_add_to_menu_composing": { + "message": "撰写电子邮件" + }, + "customPrompts_do_reply": { + "message": "请回复" + }, + "chatgpt_btn_retry": { + "message": "重试" + }, + "customPrompts_add_to_menu_reading": { + "message": "阅读电子邮件" + }, + "chatgpt_win_job_completed_select": { + "message": "选择您想要使用的文本并单击按钮。" + }, + "chatgpt_sendbutton_not_found_error": { + "message": "单击发送按钮提交提示词。" + }, + "customPrompts_substitute_text": { + "message": "替换文本" + }, + "chatgpt_win_working": { + "message": "工作正在进行中..." + }, + "prompt_spamfilter": { + "message": "检测垃圾邮件" + }, + "chatgpt_win_get_answer": { + "message": "使用选定的答案" + }, + "chatgpt_win_close": { + "message": "关闭" + }, + "prefs_OptionText_add_tags_auto": { + "message": "自动添加标签" + }, + "prefs_OptionText_spamfilter_Info": { + "message": "如果选中,ThunderAI将自动将垃圾邮件移至垃圾邮件文件夹。" + }, + "sparks_not_installed": { + "message": "ThunderAI Sparks 未安装!" + }, + "chatgpt_textarea_not_found_error": { + "message": "看起来 ChatGPT 页面加载时间太长。如果加载完成,请点击右边的按钮。如果问题仍然存在,请检查服务状态。" + }, + "prefs_OptionText_add_tags_auto_Info": { + "message": "如果选中,AI将自动为新收到的电子邮件添加标签。" + }, + "prefs_OptionText_add_tags_auto_force_existing_Info": { + "message": "如果选中,AI 将仅向新收到的电子邮件添加现有标签,而不会创建新标签。" + }, + "prefs_OptionText_add_tags_auto_only_inbox": { + "message": "仅向收件箱电子邮件添加标签" + }, + "placeholder_thunderai_def_sign": { + "message": "ThunderAI 选项中定义的默认签名。" + }, + "SpamFilter_prompt_prefs_title": { + "message": "垃圾邮件过滤选项" + }, + "prefs_OptionText_spamfilter_threshold": { + "message": "垃圾阈值" + }, + "Subject": { + "message": "主题" + }, + "chatgpt_user_not_logged_in": { + "message": "您尚未登录ChatGPT。请使用您的凭据登录,关闭ChatGPT窗口,然后重复您尝试的操作。随后您将保持登录状态。" + }, + "prefs_OptionText_default_chatgpt_lang": { + "message": "默认回复语言" + }, + "customPrompts_ExportAll": { + "message": "导出所有提示词" + }, + "ollama_empty_model": { + "message": "您尚未选择 Ollama API 的模型。请在选项页面中选择一个。" + }, + "prefs_OptionText_default_sign_name": { + "message": "默认签名名称" + }, + "prefs_OptionText_reply_all": { + "message": "回复全部" + }, + "prefsInfoDesc_6": { + "message": "您可以通过单击此页面右上角的齿轮图标并选择“管理扩展快捷方式”来更改快捷方式。" + }, + "chatgpt_win_model_warning": { + "message": "您已设置使用特定模型的选项,但似乎无法正确加载。请检查值并重试。目前,您可以按蓝色按钮继续。" + }, + "prefs_Connection_type_ChatGPT_API": { + "message": "ChatGPT OpenAI API" + }, + "Debug": { + "message": "调试" + }, + "AddTags_info_default": { + "message": "在此页面中,您可以修改用于向电子邮件添加标签的默认提示词并管理排除列表。" + }, + "OpenAIComp_api_request_failed": { + "message": "OpenAI 兼容 API 请求失败" + }, + "StorageSpace": { + "message": "占用的总存储空间" + }, + "addtags_excl_list_infoline2": { + "message": "每行添加一个词,或用逗号分隔。" + }, + "google_gemini_api_request_failed": { + "message": "Google Gemini API 请求失败" + }, + "placeholder_junk_score": { + "message": "垃圾评分" + }, + "chatgpt_win_custom_text": { + "message": "在此处插入提示词的附加文本。" + }, + "prefs_status_page": { + "message": "服务状态" + }, + "chatgpt_empty_model": { + "message": "您尚未选择 ChatGPT API 的模型。请在选项页面中选择一个。" + }, + "prefs_Connection_type_OpenAI_Comp_API": { + "message": "OpenAI 兼容的 API" + }, + "prefs_OptionText_dynamic_menu_order_alphabet_info": { + "message": "如果勾选此项,菜单中的提示将按字母顺序排列。" + }, + "chatgpt_win_send": { + "message": "发送" + }, + "prefs_OptionText_reply_sender": { + "message": "回复发件人" + }, + "chatgpt_force_completion_title": { + "message": "如果ChatGPT已完成处理但按钮未显示,请点击此处启用“使用最后答案”按钮。" + }, + "prefsInfoTitle": { + "message": "重要信息" + }, + "prefs_ChatGPT_API_Key": { + "message": "ChatGPT API 密钥" + }, + "chagpt_api_connecting": { + "message": "尝试使用提供的 API 密钥连接到 OpenAI ChatGPT" + }, + "prefs_OptionText_release_notes": { + "message": "发行说明" + }, + "prefsDonation_1": { + "message": "你喜欢这个插件吗?" + }, + "prefsDonation_2": { + "message": "考虑捐款!" + }, + "WaitingServerReponse": { + "message": "正在等待服务器响应" + }, + "OpenAIComp_Models_Error_fetching": { + "message": "尝试获取 OpenAI 兼容 API 模型时出错" + }, + "chatgpt_use_gpt35": { + "message": "使用 GPT3.5" + }, + "customPrompts_Import": { + "message": "导入新提示词" + }, + "error_connection_interrupted": { + "message": "与服务器的连接意外中断" + }, + "OpenAIComp_Models": { + "message": "OpenAI 兼容 API 模型" + }, + "OpenAIComp_empty_model": { + "message": "您尚未选择 OpenAI Compatible API 的模型。请在选项页面中选择一个。" + }, + "OpenAIComp_api_connecting": { + "message": "尝试使用主机连接到 OpenAI 兼容 API 本地服务器" + }, + "prefs_OpenAIComp_ChatName": { + "message": "对话名称" + }, + "placeholder_folder_path": { + "message": "文件夹路径" + }, + "placeholder_selected_text": { + "message": "选定的文本" + }, + "placeholder_account_email_address": { + "message": "帐户电子邮件地址" + }, + "chatgpt_force_completion": { + "message": "强制完成" + }, + "prefs_OptionText_chatgpt_win_height": { + "message": "高度" + }, + "prefs_OptionText_btnManagePrompts": { + "message": "管理您的提示词" + }, + "prefsInfoDesc_5": { + "message": "请记住,您可以使用键盘快捷键 CTRL+ALT+A 打开 ThunderAI 菜单。" + }, + "importPrompts_invalidFile": { + "message": "您尝试导入的文件不是有效的自定义提示文件。" + }, + "prefsInfoDesc_4": { + "message": "若要使用此集成功能,您需要搭建一台兼容OpenAI API的本地服务器(如LM Studio)。服务器启动后,请在应用程序中的指定字段填入其地址。为确保ThunderAI与本地服务器之间通信正常,请务必正确配置CORS设置。" + }, + "prefs_OptionText_chatgpt_win_text": { + "message": "ChatGPT 窗口尺寸" + }, + "prefs_OptionText_chatgpt_win_width": { + "message": "宽度" + }, + "prefsInfoDesc_3": { + "message": "若要使用与Ollama的集成功能,您需要先搭建本地Ollama服务器。服务器运行后,在应用程序中的指定字段输入其地址。为确保ThunderAI与Ollama服务器之间通信正常,请记得设置环境变量OLLAMA_ORIGINS=moz-extension://*。" + }, + "importPrompts_invalidPrompts": { + "message": "您尝试导入的文件不包含任何有效提示词。" + }, + "andModel": { + "message": "和模型" + }, + "ChatGPT_Models_Error_fetching": { + "message": "尝试获取 ChatGPT 模型时出错" + }, + "prefs_OptionText_dynamic_menu_order_alphabet": { + "message": "菜单:按字母顺序排列" + }, + "prefsInfoDesc_2": { + "message": "要使用 ChatGPT API,您需要一个 OpenAI ChatGPT API 密钥并且必须选择一个模型。" + }, + "TranslateText": { + "message": "你想帮助翻译这个插件吗?" + }, + "importPrompts_confirmText": { + "message": "您即将导入新的提示。" + }, + "customPrompts_start_import": { + "message": "开始导入自定义提示词..." + }, + "customPrompts_import_completed": { + "message": "自定义提示词导入完成!您需要点击“全部保存”按钮来保存您的更改。" + }, + "prefs_Connection_type": { + "message": "连接类型" + }, + "prefs_OptionText_reply_type": { + "message": "回复类型" + }, + "msg_prompt_too_long": { + "message": "您提供的文本太长。您需要缩短它。" + }, + "TranslateLink": { + "message": "了解详情!" + }, + "Loading": { + "message": "加载中..." + }, + "prefsInfoDesc_1": { + "message": "有时,ChatGPT的网页界面可能会发生更改,导致插件功能无法正常运行。请查看本页底部的“服务状态”页面链接以获取最新信息。此外,请记住,首次使用ThunderAI时,您需要先登录ChatGPT。" + }, + "prefs_OptionText_do_debug_info": { + "message": "激活调试系统" + }, + "prefs_API_Host": { + "message": "主机地址" + }, + "placeholder_author": { + "message": "作者" + }, + "Ollama_Models_Error_fetching": { + "message": "尝试获取 Ollama 模型时出错" + }, + "Optional": { + "message": "可选的" + }, + "placeholder_mail_text_body": { + "message": "邮件正文" + }, + "placeholder_additional_text": { + "message": "附加文本" + }, + "Ollama_Models_Fetch": { + "message": "更新 Ollama 模型列表" + }, + "currently_used_prompt": { + "message": "当前使用的提示词名称" + }, + "ChatGPT_Models": { + "message": "ChatGPT 模型" + }, + "chatgpt_api_request_failed": { + "message": "OpenAI ChatGPT API 请求失败" + }, + "prefs_API_Host_Info": { + "message": "类似于" + }, + "OpenAIComp_empty_host": { + "message": "您尚未添加 OpenAI 兼容 API 的主机地址。请在选项页面中添加一个。" + }, + "OpenChatGPTTab_Info": { + "message": "如果在 ThunderAI 窗口中遇到登录问题,请使用右侧的按钮在新标签页中打开 ChatGPT,完成登录后关闭该标签页,然后继续使用 ThunderAI。" + }, + "prompt_translate_this_full_text": { + "message": "将以下电子邮件翻译成" + }, + "prompt_add_tags": { + "message": "为这封电子邮件添加标签" + }, + "prefs_OptionText_add_tags_maxnum_Info": { + "message": "AI 建议的最大标签数量。如果不想限制标签数量,请设置为 0。" + }, + "prompt_rewrite_full_text": { + "message": "重写以下文字以使其更加礼貌。仅使用重写的文字进行回复,不要添加额外的评论或其他文字。" + }, + "customprompts_form_label_define_response_lang": { + "message": "在提示词中定义响应语言" + }, + "prefs_OptionText_dynamic_menu_force_enter": { + "message": "菜单:立即发送提示词" + }, + "prefs_OptionText_dynamic_menu_force_enter_info": { + "message": "如果勾选此项,使用键盘快捷键 CTRL+ALT+A 将自动从菜单中发送高亮显示的提示。否则,会向用户显示提示名称,需要再次按下 Enter 键才能发送。" + }, + "OpenChatGPTTab": { + "message": "打开 ChatGPT 标签" + }, + "placeholder_recipients": { + "message": "收件人" + }, + "prefs_OptionText_owl_warning": { + "message": "看来您的至少一个账户正在使用 Owl for Exchange 插件。Thunderbird 和 Owl 之间存在一个已知问题,目前正在解决中。在此期间,您可以在撰写邮件时使用 ThunderAI,但在阅读邮件时无法使用。" + }, + "prefs_Connection_type_ChatGPT_Web": { + "message": "ChatGPT Web 界面" + }, + "ChatGPT_Models_Fetch": { + "message": "更新 ChatGPT 模型列表" + }, + "SearchPrompt": { + "message": "搜索提示词" + }, + "error": { + "message": "ThunderAI 错误!" + }, + "ollama_empty_host": { + "message": "您尚未为 Ollama API 添加主机地址。请在选项页面中增加一个。" + }, + "chatgpt_empty_apikey": { + "message": "您尚未添加 ChatGPT API 的 API 密钥。请在选项页面中添加一个。" + }, + "chagpt_api_send_button": { + "message": "使用中的模型" + }, + "GoogleGemini_Models": { + "message": "Google Gemini API 模型" + }, + "google_gemini_empty_apikey": { + "message": "您尚未添加 Google Gemini API 的 API 密钥。请在选项页面中填入一个。" + }, + "API_Models_Error_NoModels": { + "message": "未找到任何模型" + }, + "prefs_Connection_type_Ollama_API": { + "message": "Ollama API (本地模型)" + }, + "placeholder_mail_html_body": { + "message": "HTML 邮件正文" + }, + "placeholder_cc_list": { + "message": "抄送列表" + }, + "prefs_OptionText_chatgpt_web_model_tooltip": { + "message": "单击一个值进行设置。" + }, + "prefs_OpenAIComp_ForceModel": { + "message": "手动填入模型" + }, + "Ollama_Models": { + "message": "Ollama 模型" + }, + "OpenAIComp_force_model_ask": { + "message": "在此处填入您想要使用的模型名称。" + }, + "ollama_api_connecting": { + "message": "尝试使用主机连接到 Ollama 本地服务器" + }, + "ollama_api_request_failed": { + "message": "Ollama API 请求失败" + }, + "prefs_OpenAIComp_API_Key": { + "message": "OpenAI 兼容 API 密钥" + }, + "OpenAIComp_Models_Fetch": { + "message": "更新 OpenAI 兼容 API 模型列表" + }, + "prefs_OptionText_chatgpt_win_dims_info": { + "message": "如果不想指定窗口大小,请设置为 0。" + }, + "placeholder_tags_current_email": { + "message": "邮件标签" + }, + "prefs_GoogleGemini_API_Key": { + "message": "API 密钥" + }, + "prompt_reply_advanced_full_text": { + "message": "回复以下邮件“{%selected_text%}”,并思考整个邮件主题“{%mail_html_body%}”。仅回复所需文本,不添加任何额外评论或其他文字。" + }, + "addtags_info_additional_statements": { + "message": "该声明将添加到提示词的末尾:" + }, + "prefs_Connection_type_Google_Gemini_API": { + "message": "Google Gemini API" + }, + "prefs_OpenAIComp_ChatName_Info": { + "message": "这是将在 AI 聊天中使用的名字。" + }, + "prefs_OptionText_openai_comp_use_v1_info": { + "message": "如果选中,API 调用路径中的“v1”段将被保留,例如“http://localhost:1234/v1/chat/completions”。" + }, + "placeholder_mail_subject": { + "message": "邮件主题" + }, + "placeholder_folder_name": { + "message": "文件夹名称" + }, + "prefs_OptionText_placeholders_use_default_value": { + "message": "占位符:使用默认值" + }, + "thunderai_def_lang": { + "message": "ThunderAI 选项中定义的默认语言。" + }, + "prefs_OptionText_openai_comp_info_remote": { + "message": "您还可以在此处插入远程服务器地址。" + }, + "thunderai_warning_title": { + "message": "ThunderAI 警告" + }, + "google_gemini_api_connecting": { + "message": "尝试使用提供的 API 密钥连接到 Google Gemini" + }, + "prefs_SurveyLinkText2": { + "message": "单击此处,只需一分钟!" + }, + "prompt_add_tags_full_text": { + "message": "分析以下邮件文本,并生成一个用逗号分隔的标签列表,以概括其内容。使用主题、关键话题和相关描述词作为标签。确保标签简洁且与邮件内容相关。\n邮件文本:{%mail_text_body%}\n考虑以下细节以获取上下文:\n- 发件人:{%author%}\n- 收件人:{%recipients%}\n- 抄送列表:{%cc_list%}\n- 邮件主题:{%mail_subject%}\n请根据邮件文本和上下文生成标签,忽略不必要的信息或琐碎的细节。输出应仅为逗号分隔的标签列表,不包含任何额外评论或文字。" + }, + "placeholder_tags_full_list": { + "message": "现有标签" + }, + "addtags_dialog_title": { + "message": "为电子邮件添加标签" + }, + "prompt_summarize_this_full_text": { + "message": "将以下电子邮件总结为要点列表。" + }, + "prompt_rewrite_formal_full_text": { + "message": "重写以下文字,使其更加正式。回复时只使用重写的文字,不要添加任何额外的评论或其他文字。" + }, + "prompt_this_full_text": { + "message": "仅使用所需的文本进行回复,不需要额外的评论或其他文本。" + }, + "prefs_OptionText_btnManageTagsInfo": { + "message": "管理标签设置" + }, + "prompt_add_tags_maxnum": { + "message": "将标签数量限制为" + }, + "AddTags_PageTitle": { + "message": "管理标签设置" + }, + "reply_same_lang": { + "message": "用相同的语言回复。" + }, + "prompt_classify_full_text": { + "message": "按照礼貌、热情、正式、自信、冒犯性对以下文本进行分类,并为每个类别给出百分比。仅回复类别并评分,不要添加额外评论或其他文本。" + }, + "prefs_OptionText_add_tags": { + "message": "为电子邮件添加标签" + }, + "prefs_OptionText_add_tags_Info": { + "message": "如果选中,菜单中将包含一项用于将标签应用于电子邮件的项目。" + }, + "prefs_OptionText_add_tags_maxnum": { + "message": "最大标签数" + }, + "prefs_OptionText_add_tags_hide_exclusions": { + "message": "隐藏排除的标签" + }, + "prefs_OptionText_add_tags_hide_exclusions_Info": { + "message": "如果选中,位于排除列表中的标签将在确认对话框中隐藏。" + }, + "AddTags_prompt_text_title": { + "message": "当前提示文字" + }, + "save": { + "message": "保存" + }, + "prefs_OptionText_add_tags_force_lang_Info": { + "message": "如果选中,标签语言将被强制与 ThunderAI 选项页面上定义的语言匹配(如果指定)。" + }, + "GoogleGemini_SystemInstruction_Info": { + "message": "当您设置系统指令时,您为模型提供了额外的上下文,以帮助其理解任务、提供更个性化的响应,并遵循将在提示中发送的具体准则。" + }, + "prompt_get_calendar_event": { + "message": "添加新日历事件" + }, + "AddTags_excl_list_title": { + "message": "排除列表" + }, + "google_gemini_empty_model": { + "message": "您尚未选择 Google Gemini API 的模型。请在选项页面中选择一个。" + }, + "From": { + "message": "来自" + }, + "prefs_OptionText_add_tags_first_uppercase": { + "message": "首字母大写" + }, + "GoogleGemini_SystemInstruction": { + "message": "系统指令" + }, + "reset_default": { + "message": "重置为默认值" + }, + "AddTags_excl_list_infoline": { + "message": "这是一份禁止添加到邮件中的标签列表。" + }, + "prefs_OptionText_btnManageSpamFilterInfo": { + "message": "管理垃圾邮件过滤器设置" + }, + "addtags_no_tags_received": { + "message": "未收到来自 AI 的标签。" + }, + "addtags_no_valid_tags": { + "message": "使用排除列表过滤后,未找到有效标签。" + }, + "prefs_SurveyLinkText": { + "message": "分享您的反馈并帮助改进 ThunderAI!" + }, + "GoogleGemini_Models_Error_fetching": { + "message": "尝试获取 Google Gemini 模型时出错" + }, + "ChatGPT_Developer_Messages": { + "message": "开发者消息" + }, + "prefs_OptionText_download_now": { + "message": "立即下载ThunderAI Sparks!" + }, + "thunderai_error_title": { + "message": "ThunderAI 错误" + }, + "prefs_OptionText_add_tags_first_uppercase_Info": { + "message": "如果选中,标签标签将设置为小写字母,但只有首字母大写。" + }, + "AddTags_prompt_prefs_title": { + "message": "添加标签选项" + }, + "prefs_OptionText_add_tags_force_lang": { + "message": "强制语言" + }, + "GoogleGemini_Models_Fetch": { + "message": "更新 Google Gemini 模型列表" + }, + "ChatGPT_Developer_Messages_Info": { + "message": "当您设置开发者消息时,您为模型提供了额外的上下文,以帮助其理解任务、提供更个性化的响应,并遵循将在提示中发送的具体准则。" + }, + "SpamFilter_info_default": { + "message": "在此页面中您可以修改用于检测垃圾邮件的默认提示词。" + }, + "prefs_OptionText_btnManagePrompts_infoline3": { + "message": "您可以在提示中使用 {%tags_full_list%} 数据占位符来列出可用的标签。通过适当的提示,您可以强制仅从现有标签列表中选择标签。" + }, + "placeholder_mail_typed_text": { + "message": "在引用的邮件正文前输入文本" + }, + "calendar_opening_dialog_error": { + "message": "打开日历事件对话框时出错" + }, + "prefs_OptionText_spamfilter": { + "message": "自动垃圾邮件过滤器" + }, + "SpamFilter_PageTitle": { + "message": "管理垃圾邮件过滤器设置" + }, + "prompt_get_calendar_event_full_text": { + "message": "从以下文本中提取生成日历事件所需的所有相关细节。提取的信息应包括:\n- 事件标题\n- 开始日期和时间(如果指定时区,则包括时区)\n- 结束日期和时间(如果指定时区,则包括时区)\n- 全天事件(如果提及)\n确保数据格式清晰且一致,以便可以直接用于创建日历事件。\n如果存在相对时间的引用,请注意邮件的日期和时间为“{%mail_datetime%}”。基于此参考计算开始日期和时间。如果计算出的开始日期和时间早于“{%current_datetime%}”,则使用“{%current_datetime%}”作为基准重新计算开始日期和时间。\n如果未指定持续时间,请将其设置为一小时。\n如果无法获取一个或多个所需信息,请以空字符串响应。\n仅以 JSON 格式生成响应。不要包含任何额外的文本或说明,仅提供 JSON。以下是使用的格式:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"日历事件摘要\",\n\"forceAllDay\": false\n}\n以下是文本:“{%selected_text%}”" + }, + "prefs_OptionText_get_calendar_event": { + "message": "从所选文本添加新日历事件" + }, + "prefs_OptionText_get_calendar_event_Info": { + "message": "如果选中,菜单中将包含一个项目,以从选定的文本中获取日历事件信息。" + }, + "prefs_OptionText_btnManageCalendarEventInfo": { + "message": "管理日历活动设置" + }, + "GetCalendarEvent_info_default": { + "message": "在此页面中,您可以修改用于从选定文本获取日历事件的默认提示。" + }, + "prefs_OptionText_add_tags_auto_force_existing": { + "message": "自动标记时强制使用现有标签" + }, + "prefs_OptionText_add_tags_auto_only_inbox_Info": { + "message": "如果选中,AI 将仅向收件箱文件夹中收到的电子邮件添加标签。" + }, + "GetCalendarEvent_PageTitle": { + "message": "管理日历活动设置" + }, + "prefs_OptionText_get_calendar_event_Sparks_not_present": { + "message": "要使用日历事件功能,请安装 ThunderAI Sparks 插件。" + }, + "placeholder_mail_datetime": { + "message": "电子邮件的日期和时间" + }, + "SpamFilter_prompt_text_title": { + "message": "当前提示词" + }, + "placeholder_current_datetime": { + "message": "当前日期和时间" + }, + "GetCalendarEvent_prompt_text_title": { + "message": "当前提示词文字" + }, + "prefs_OptionText_GetCalendarEvent_infoline2": { + "message": "您可以根据需要更改提示,但从 AI 收到的响应必须采用默认提示中指定的 JSON 格式!" + }, + "calendar_getting_data_error": { + "message": "获取日历事件数据时出错" + }, + "prefs_OptionText_spamfilter_infoline": { + "message": "您可以根据需要更改提示词,但从 AI 收到的响应必须采用默认提示中指定的 JSON 格式!" + }, + "prompt_spamfilter_full_text": { + "message": "分析以下邮件并判断它是否为垃圾邮件。考虑因素包括可疑关键词、过多的宣传语言、误导性的主题行、索取个人信息的请求以及异常的发件人地址。\n提供一个从 0(非垃圾邮件)到 100(垃圾邮件)的值,并附上不超过 10 个单词的解释。\n仅以 JSON 格式生成响应。不要包含任何额外文本或解释;仅提供 JSON。以下是使用的格式:\n{\n\"spamValue\": <0 到 100 的整数>,\n\"explanation\": \"简要说明您的判断理由\",\n}\n以下是邮件信息:\n发件人:“{%author%}”\n主题:“{%mail_subject%}”\nHTML 正文:“{%mail_html_body%}”" + }, + "prefs_OptionText_spamfilter_threshold_Info": { + "message": "如果 AI 返回的值高于此阈值,电子邮件将被移至垃圾邮件文件夹。" + }, + "spamfilter_threshold_too_low": { + "message": "垃圾邮件阈值太低!您可能会将太多邮件标记为垃圾邮件!" + }, + "Spam_Value": { + "message": "垃圾指数" + }, + "spamfilter_threshold_zero": { + "message": "垃圾邮件阈值为零!您将把所有邮件标记为垃圾邮件!" + }, + "Date": { + "message": "日期" + }, + "Moved_to_Spam": { + "message": "移至垃圾邮件箱" + }, + "Report_Date": { + "message": "报告日期" + } +} From 1be7bfe5eedb4218798aee0d258ad2d9e22ac999 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 15 Mar 2025 08:58:07 +0100 Subject: [PATCH 19/92] release notes updated --- CHANGELOG.md | 2 ++ options/mzta-release-notes.html | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 691b1397..66335f71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • Improved the handling of null or undefined placeholders [#288].
  • +
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • +
  • Simplified Chinese (zh_Hans) translation added, thanks to jeklau.
  • ...
  • Version 3.2.3 - 06/03/2025

    diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index c1e7c066..35904484 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -13,6 +13,8 @@
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • Improved the handling of null or undefined placeholders [#288].
  • +
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • +
  • Simplified Chinese (zh_Hans) translation added, thanks to jeklau.
  • ...
  • Version 3.2.3 - 06/03/2025

    From 8d7469b4c5c4a37244aca140a9f48698f7727b00 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 15 Mar 2025 08:58:50 +0100 Subject: [PATCH 20/92] production languages list updated --- LANG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LANG.md b/LANG.md index 2065a3ed..576fab13 100644 --- a/LANG.md +++ b/LANG.md @@ -4,4 +4,6 @@ fr hr it pl -pt-br \ No newline at end of file +pt-br +cs +zh_Hans \ No newline at end of file From c26b182ae9c28f8985da01c35b5c917e1bec98b1 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 16 Mar 2025 23:28:48 +0100 Subject: [PATCH 21/92] Add context menu options for tags and spam filter. see #262 --- options/mzta-options-default.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 05a11c2d..b92a1f66 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -50,7 +50,9 @@ export const prefs_default = { add_tags_auto: false, add_tags_auto_force_existing: false, add_tags_auto_only_inbox: true, + add_tags_context_menu: true, get_calendar_event: true, spamfilter: false, spamfilter_threshold: 70, + spamfilter_context_menu: true, } \ No newline at end of file From d200147f41eae5b90146f568daa24b8332e1feb0 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 16 Mar 2025 23:29:05 +0100 Subject: [PATCH 22/92] Add checkbox option for context menu in add tags settings. see #262 --- pages/addtags/mzta-add-tags.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pages/addtags/mzta-add-tags.html b/pages/addtags/mzta-add-tags.html index 7d15a704..9067132c 100644 --- a/pages/addtags/mzta-add-tags.html +++ b/pages/addtags/mzta-add-tags.html @@ -76,6 +76,15 @@ + + __MSG_prefs_OptionText_add_tags_context_menu__ + + + +
    __MSG_AddTags_prompt_text_title__ From 87e93dd178c770e08b7c8927cc829ad44c585b66 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 16 Mar 2025 23:32:03 +0100 Subject: [PATCH 23/92] Add checkbox option for spam filter context menu. see #262 --- pages/spamfilter/mzta-spamfilter.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pages/spamfilter/mzta-spamfilter.html b/pages/spamfilter/mzta-spamfilter.html index 2a2f2846..3ccf6155 100644 --- a/pages/spamfilter/mzta-spamfilter.html +++ b/pages/spamfilter/mzta-spamfilter.html @@ -22,6 +22,15 @@ + + __MSG_prefs_OptionText_spamfilter_context_menu__ + + + +
    __MSG_GetCalendarEvent_prompt_text_title__ From 5aeb95f880133a6c74259141ab52ff84fce2b52c Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 16 Mar 2025 23:32:33 +0100 Subject: [PATCH 24/92] Add context menu IDs for add tags and spam filter. see #262 --- js/mzta-utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/mzta-utils.js b/js/mzta-utils.js index 7fb5d8ad..d024637f 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -19,6 +19,8 @@ export const getMenuContextCompose = () => 'compose_action_menu'; export const getMenuContextDisplay = () => 'message_display_action_menu'; +export const contextMenuID_AddTags = () => 'mzta-add-tags'; +export const contextMenuID_Spamfilter = () => 'mzta-spamfilter'; export function getLanguageDisplayName(languageCode) { const languageDisplay = new Intl.DisplayNames([languageCode], {type: 'language'}); From 72ccc7de903902964659cf891ef9252f76df36c5 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 16 Mar 2025 23:32:48 +0100 Subject: [PATCH 25/92] Add context menu creation and removal functions. see #262 --- mzta-background.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/mzta-background.js b/mzta-background.js index a1634c16..33870fa5 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -20,7 +20,7 @@ import { mzta_script } from './js/mzta-chatgpt.js'; import { prefs_default } from './options/mzta-options-default.js'; import { mzta_Menus } from './js/mzta-menus.js'; import { taLogger } from './js/mzta-logger.js'; -import { getCurrentIdentity, getOriginalBody, replaceBody, setBody, i18nConditionalGet, generateCallID, migrateCustomPromptsStorage, migrateDefaultPromptsPropStorage, getGPTWebModelString, getTagsList, createTag, assignTagsToMessage, checkIfTagExists, getActiveSpecialPromptsIDs, checkSparksPresence, getMessages, getMailBody, extractJsonObject } from './js/mzta-utils.js'; +import { getCurrentIdentity, getOriginalBody, replaceBody, setBody, i18nConditionalGet, generateCallID, migrateCustomPromptsStorage, migrateDefaultPromptsPropStorage, getGPTWebModelString, getTagsList, createTag, assignTagsToMessage, checkIfTagExists, getActiveSpecialPromptsIDs, checkSparksPresence, getMessages, getMailBody, extractJsonObject, contextMenuID_AddTags, contextMenuID_Spamfilter } from './js/mzta-utils.js'; import { taPromptUtils } from './js/mzta-utils-prompt.js'; import { mzta_specialCommand } from './js/mzta-special-commands.js'; import { getSpamFilterPrompt } from './js/mzta-prompts.js'; @@ -681,6 +681,20 @@ setupPermissionsRemovedListener(); const menus = new mzta_Menus(openChatGPT, prefs_init.do_debug); menus.loadMenus(special_prompts_ids); +// Context Menus +function addContextMenu(menu_id) { + browser.menus.create({ + id: menu_id, + title: browser.i18n.getMessage("context_menu_" + menu_id), + contexts: ["message_list"] + }); + taLog.log("Context menu added: " + menu_id); +} + +function removeContextMenu(menu_id) { + browser.menus.remove(menu_id); + taLog.log("Context menu removed: " + menu_id); +} // Listening for new received emails const newEmailListener = (folder, messagesList) => { From d618b3c70ff657332081346064a7c37b24c4b933 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 16 Mar 2025 23:32:53 +0100 Subject: [PATCH 26/92] en locale updated --- _locales/en/messages.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1fdc0e5b..83c9e21b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1166,5 +1166,29 @@ "spamfilter_not_moved": { "message": "No", "description": "" + }, + "context_menu_mzta-add-tags": { + "message": "Add tags", + "description": "" + }, + "context_menu_spamfilter": { + "message": "Analyze for spam", + "description": "" + }, + "prefs_OptionText_add_tags_context_menu": { + "message": "Show the \"Add tags\" context menu item", + "description": "" + }, + "prefs_OptionText_add_tags_context_menu_Info": { + "message": "If checked, the \"Add tags\" context menu item will be shown when right-clicking on an email in the messages list.", + "description": "" + }, + "prefs_OptionText_spamfilter_context_menu": { + "message": "Show the \"Analyze for spam\" context menu item", + "description": "" + }, + "prefs_OptionText_spamfilter_context_menu_Info": { + "message": "If checked, the \"Analyze for spam\" context menu item will be shown when right-clicking on an email in the messages list.", + "description": "" } } \ No newline at end of file From 0ed6da631a236c4fb7c2d675ece55edfcb9f3b67 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 18 Mar 2025 21:38:50 +0100 Subject: [PATCH 27/92] Rename context menu entry for spam filter to improve clarity --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 83c9e21b..6c264095 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1171,7 +1171,7 @@ "message": "Add tags", "description": "" }, - "context_menu_spamfilter": { + "context_menu_mzta-spamfilter": { "message": "Analyze for spam", "description": "" }, From 109cd66c420033b2d5845625bdd8769747eae609 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 18 Mar 2025 21:38:57 +0100 Subject: [PATCH 28/92] Refactor context menu ID exports to use direct assignment --- js/mzta-utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/mzta-utils.js b/js/mzta-utils.js index d024637f..decad36c 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -19,8 +19,8 @@ export const getMenuContextCompose = () => 'compose_action_menu'; export const getMenuContextDisplay = () => 'message_display_action_menu'; -export const contextMenuID_AddTags = () => 'mzta-add-tags'; -export const contextMenuID_Spamfilter = () => 'mzta-spamfilter'; +export const contextMenuID_AddTags = 'mzta-add-tags'; +export const contextMenuID_Spamfilter = 'mzta-spamfilter'; export function getLanguageDisplayName(languageCode) { const languageDisplay = new Intl.DisplayNames([languageCode], {type: 'language'}); From 08f9fef402db01a85af6d7f0aab0b2c0675e6c1e Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 18 Mar 2025 22:26:25 +0100 Subject: [PATCH 29/92] fix failSafePlaceholders function to return element if defined --- js/mzta-placeholders.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/mzta-placeholders.js b/js/mzta-placeholders.js index d8a0064c..d8f2079b 100644 --- a/js/mzta-placeholders.js +++ b/js/mzta-placeholders.js @@ -357,9 +357,11 @@ export const placeholdersUtils = { }, failSafePlaceholders(element){ + //console.log(">>>>>>>>>> failSafePlaceholders element: " + JSON.stringify(element)); if(element === null || element === undefined){ return ''; } + return element; } } \ No newline at end of file From 717f035803cdbc6320e2aae266e16dceb92de73d Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 18 Mar 2025 22:29:17 +0100 Subject: [PATCH 30/92] added processEmails method --- mzta-background.js | 229 ++++++++++++++++++++------------------------- 1 file changed, 100 insertions(+), 129 deletions(-) diff --git a/mzta-background.js b/mzta-background.js index 33870fa5..f371ab97 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -27,7 +27,7 @@ import { getSpamFilterPrompt } from './js/mzta-prompts.js'; import { taSpamReport } from './js/mzta-spamreport.js'; browser.runtime.onInstalled.addListener(({ reason, previousVersion }) => { - console.log(">>>>>>>>>>> onInstalled: " + JSON.stringify(reason) + ", previousVersion: " + previousVersion); + // console.log(">>>>>>>>>>> onInstalled: " + JSON.stringify(reason) + ", previousVersion: " + previousVersion); if (reason === "install" || (reason === "update" && (previousVersion.startsWith("2.") || previousVersion.startsWith("1."))) || (reason === "update" && ((previousVersion.startsWith("3.") && parseInt(previousVersion.split(".")[1]) <= 2))) @@ -614,7 +614,7 @@ async function doGetCalendarEvent(get_calendar_event) { } async function reload_pref_init(){ - prefs_init = await browser.storage.sync.get({do_debug: false, add_tags: true, get_calendar_event: true, connection_type: 'chatgpt_web', add_tags_auto: false, add_tags_auto_force_existing: false, add_tags_auto_only_inbox: true, spamfilter: false, spamfilter_threshold: 70, dynamic_menu_force_enter: false}); + prefs_init = await browser.storage.sync.get({do_debug: false, add_tags: true, get_calendar_event: true, connection_type: 'chatgpt_web', add_tags_auto: false, add_tags_auto_force_existing: false, add_tags_auto_only_inbox: true, spamfilter: false, spamfilter_threshold: 70, dynamic_menu_force_enter: false, add_tags_context_menu: true, spamfilter_context_menu: true}); } @@ -696,6 +696,17 @@ function removeContextMenu(menu_id) { taLog.log("Context menu removed: " + menu_id); } +// Add Context menu: Add tags +if(prefs_init.add_tags_context_menu){ + addContextMenu(contextMenuID_AddTags); +} + +// Add Context menu: Spamfilter +if(prefs_init.spamfilter_context_menu){ + addContextMenu(contextMenuID_Spamfilter); +} + + // Listening for new received emails const newEmailListener = (folder, messagesList) => { taLog.log("New mail received"); @@ -706,132 +717,7 @@ const newEmailListener = (folder, messagesList) => { taSpamReport.logger = taLog; - for await (let message of messages) { - // let fullMessage = await browser.messages.getFull(message.id); - // taLog.log(`From: ${fullMessage.headers.from}`); - // taLog.log(`Subject: ${fullMessage.headers.subject}`); - // taLog.log(`Name: ${fullMessage.name}`); - // taLog.log(`Body: ${fullMessage.body}`); - // taLog.log(`contentType: [${fullMessage.contentType}]`); - // taLog.log(`fullMessage.parts.length: ${fullMessage.parts.length}`); - - // if (fullMessage.parts.length > 0) { - // for (let part of fullMessage.parts) { - // taLog.log(`From: ${part.headers.from}`); - // taLog.log(`Subject: ${part.headers.subject}`); - // taLog.log(`Name: ${part.name}`); - // taLog.log(`Body: ${part.body}`); - // taLog.log(`contentType: [${part.contentType}]`); - // taLog.log('contentType JSON: '+JSON.stringify(part.contentType)); - // if (part.contentType.trim().toLowerCase() === "text/plain") { - // taLog.log("Body (text/plain):" + part.body); - // // return part.body; - // } - // if (part.contentType.trim().toLowerCase() === "text/html") { - // taLog.log("Body (text/html):" + part.body); - // // return part.body; - // } - // } - // } - - let curr_fullMessage = null; - let msg_text = null; - let body_text = ''; - - // if some auto feature is active prepare some data - // this will be used when will be implemented also the auto translator https://github.com/micz/ThunderAI/issues/247 - if(prefs_init.add_tags_auto || prefs_init.spamfilter){ - curr_fullMessage = await browser.messages.getFull(message.id); - msg_text = await getMailBody(curr_fullMessage); - body_text = msg_text.text.replace(/\s+/g, ' ').trim(); - } - - // Auto add_tags - if(prefs_init.add_tags_auto){ - let specialFullPrompt_add_tags = ''; - let curr_prompt_add_tags = menus.allPrompts.find(p => p.id === 'prompt_add_tags'); - let tags_full_list = await getTagsList(); - let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_add_tags); - specialFullPrompt_add_tags = await taPromptUtils.preparePrompt(curr_prompt_add_tags, message, chatgpt_lang, /*selection_text*/ '', body_text, curr_fullMessage.headers.subject, msg_text, /*only_typed_text*/ '', tags_full_list); - let prefs_aat = await browser.storage.sync.get({add_tags_maxnum: 3, connection_type: '', add_tags_force_lang: true, default_chatgpt_lang: '', add_tags_auto_force_existing: false}); - specialFullPrompt_add_tags = taPromptUtils.finalizePrompt_add_tags(specialFullPrompt_add_tags, prefs_aat.add_tags_maxnum, prefs_aat.add_tags_force_lang, prefs_aat.default_chatgpt_lang); - taLog.log("Special prompt: " + specialFullPrompt_add_tags); - let cmd_addTags = new mzta_specialCommand(specialFullPrompt_add_tags,prefs_aat.connection_type,prefs_init.do_debug); - await cmd_addTags.initWorker(); - let tags_current_email = ''; - try{ - tags_current_email = (await cmd_addTags.sendPrompt()).trim(); - }catch(err){ - console.error("[ThunderAI | Auto add_tags] Error getting tags: ", err); - } - taLog.log("tags_current_email: " + tags_current_email); - let _data = {messageId: message.id, tags: tags_current_email.split(/,\s*/)}; - _assign_tags(_data, !prefs_aat.add_tags_auto_force_existing); - } - - - // Spam filter - if(prefs_init.spamfilter){ - let curr_prompt_spamfilter = await getSpamFilterPrompt(); - //console.log(">>>>>>>>>>>> curr_prompt_spamfilter: " + JSON.stringify(curr_prompt_spamfilter)); - let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_spamfilter); - let specialFullPrompt_spamfilter = await taPromptUtils.preparePrompt(curr_prompt_spamfilter, message, chatgpt_lang, /*selection_text*/ '', body_text, curr_fullMessage.headers.subject, msg_text, /*only_typed_text*/ '', ''); - taLog.log("Special prompt: " + specialFullPrompt_spamfilter); - let cmd_spamfilter = new mzta_specialCommand(specialFullPrompt_spamfilter,prefs_init.connection_type,prefs_init.do_debug); - await cmd_spamfilter.initWorker(); - /* We expect to receive from the AI a JSON object like this: - * { - * "spamValue": , - * "explanation": "Brief explanation of your reasoning", - * } - */ - let spamfilter_result = ''; - taLog.log("Sending the prompt..."); - try{ - spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim(); - }catch(err){ - console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err); - } - taLog.log("spamfilter_result: " + spamfilter_result); - let jsonObj = {}; - taLog.log("Decoding the AI response..."); - try{ - jsonObj = extractJsonObject(spamfilter_result); - }catch(e){ - console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e); - } - taLog.log("SpamFilter jsonObj: " + JSON.stringify(jsonObj)); - - let report_data = {}; - report_data.report_date = new Date(); - report_data.headerMessageId = message.headerMessageId; - report_data.spamValue = jsonObj.spamValue; - report_data.explanation = jsonObj.explanation; - report_data.subject = curr_fullMessage.headers.subject; - report_data.from = curr_fullMessage.headers.from; - report_data.message_date = new Date(message.date); - report_data.moved = false; - report_data.SpamThreshold = prefs_init.spamfilter_threshold; - //console.log(">>>>>>>>>>>> report_data.SpamThreshold: " + JSON.stringify(report_data.SpamThreshold)); - - //TODO move email if it's spam - if(jsonObj.spamValue >= prefs_init.spamfilter_threshold){ - taLog.log("Marking as spam ["+message.headerMessageId+"]"); - // mark as spam - messenger.messages.update(message.id, { junk: true }); - //get the spam folder - let spamFolder = await messenger.folders.query({accountId: message.folder.accountId, specialUse: ['junk']}) - //console.log(">>>>>>>>>>>> spamFolder: " + JSON.stringify(spamFolder)); - //move the message - messenger.messages.move([message.id], spamFolder[0].id); - report_data.moved = true; - taLog.log("Marked as spam ["+message.headerMessageId+"]"); - } - - // Save the operation log - taSpamReport.saveReportData(report_data,message.headerMessageId); - } - } + await processEmails(messages, prefs_init.add_tags_auto, prefs_init.spamfilter); if(prefs_init.spamfilter){ taSpamReport.truncReportData(); @@ -841,9 +727,94 @@ const newEmailListener = (folder, messagesList) => { return _newEmailListener(); } +async function processEmails(messages, addTagsAuto, spamFilter) { + for await (let message of messages) { + let curr_fullMessage = null; + let msg_text = null; + let body_text = ''; + + if (addTagsAuto || spamFilter) { + curr_fullMessage = await browser.messages.getFull(message.id); + msg_text = await getMailBody(curr_fullMessage); + body_text = msg_text.text.replace(/\s+/g, ' ').trim(); + } + + if (addTagsAuto) { + let specialFullPrompt_add_tags = ''; + let curr_prompt_add_tags = menus.allPrompts.find(p => p.id === 'prompt_add_tags'); + let tags_full_list = await getTagsList(); + let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_add_tags); + specialFullPrompt_add_tags = await taPromptUtils.preparePrompt(curr_prompt_add_tags, message, chatgpt_lang, '', body_text, curr_fullMessage.headers.subject, msg_text, '', tags_full_list); + let prefs_aat = await browser.storage.sync.get({ add_tags_maxnum: 3, connection_type: '', add_tags_force_lang: true, default_chatgpt_lang: '', add_tags_auto_force_existing: false }); + specialFullPrompt_add_tags = taPromptUtils.finalizePrompt_add_tags(specialFullPrompt_add_tags, prefs_aat.add_tags_maxnum, prefs_aat.add_tags_force_lang, prefs_aat.default_chatgpt_lang); + taLog.log("Special prompt: " + specialFullPrompt_add_tags); + let cmd_addTags = new mzta_specialCommand(specialFullPrompt_add_tags, prefs_aat.connection_type, prefs_init.do_debug); + await cmd_addTags.initWorker(); + let tags_current_email = ''; + try { + tags_current_email = (await cmd_addTags.sendPrompt()).trim(); + } catch (err) { + console.error("[ThunderAI | Auto add_tags] Error getting tags: ", err); + } + taLog.log("tags_current_email: " + tags_current_email); + let _data = { messageId: message.id, tags: tags_current_email.split(/,\s*/) }; + _assign_tags(_data, !prefs_aat.add_tags_auto_force_existing); + } + + if (spamFilter) { + let curr_prompt_spamfilter = await getSpamFilterPrompt(); + let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_spamfilter); + let specialFullPrompt_spamfilter = await taPromptUtils.preparePrompt(curr_prompt_spamfilter, message, chatgpt_lang, '', body_text, curr_fullMessage.headers.subject, msg_text, '', ''); + taLog.log("Special prompt: " + specialFullPrompt_spamfilter); + let cmd_spamfilter = new mzta_specialCommand(specialFullPrompt_spamfilter, prefs_init.connection_type, prefs_init.do_debug); + await cmd_spamfilter.initWorker(); + let spamfilter_result = ''; + taLog.log("Sending the prompt..."); + try { + spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim(); + } catch (err) { + console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err); + } + taLog.log("spamfilter_result: " + spamfilter_result); + let jsonObj = {}; + taLog.log("Decoding the AI response..."); + try { + jsonObj = extractJsonObject(spamfilter_result); + } catch (e) { + console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e); + } + taLog.log("SpamFilter jsonObj: " + JSON.stringify(jsonObj)); + + let report_data = {}; + report_data.report_date = new Date(); + report_data.headerMessageId = message.headerMessageId; + report_data.spamValue = jsonObj.spamValue; + report_data.explanation = jsonObj.explanation; + report_data.subject = curr_fullMessage.headers.subject; + report_data.from = curr_fullMessage.headers.from; + report_data.message_date = new Date(message.date); + report_data.moved = false; + report_data.SpamThreshold = prefs_init.spamfilter_threshold; + + if (jsonObj.spamValue >= prefs_init.spamfilter_threshold) { + taLog.log("Marking as spam [" + message.headerMessageId + "]"); + messenger.messages.update(message.id, { junk: true }); + let spamFolder = await messenger.folders.query({ accountId: message.folder.accountId, specialUse: ['junk'] }); + messenger.messages.move([message.id], spamFolder[0].id); + report_data.moved = true; + taLog.log("Marked as spam [" + message.headerMessageId + "]"); + } + + taSpamReport.saveReportData(report_data, message.headerMessageId); + } + } +} + + + try { browser.messages.onNewMailReceived.addListener(newEmailListener, !prefs_init.add_tags_auto_only_inbox); } catch (e) { taLog.log("Using browser.messages.onNewMailReceived.addListener with one agrument for Thunderbird 115."); browser.messages.onNewMailReceived.addListener(newEmailListener); -} \ No newline at end of file +} From edf7eabdff2b12fe97021c554b6376d8e552d5a7 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 18 Mar 2025 22:36:33 +0100 Subject: [PATCH 31/92] Add context menu click listener to process emails with tags and spam filter. see #262 --- mzta-background.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mzta-background.js b/mzta-background.js index f371ab97..87385ccf 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -707,6 +707,22 @@ if(prefs_init.spamfilter_context_menu){ } +// Listen for context menu item clicks +browser.menus.onClicked.addListener( (info, tab) => { + let _add_tags = false + let _spamfilter = false + if(info.menuItemId === contextMenuID_AddTags){ + _add_tags = true; + } + if(info.menuItemId === contextMenuID_Spamfilter){ + _spamfilter = true; + } + if(_add_tags || _spamfilter){ + processEmails(getMessages(info.selectedMessages), _add_tags, _spamfilter); + } +}); + + // Listening for new received emails const newEmailListener = (folder, messagesList) => { taLog.log("New mail received"); From 7c799a65b99aa154f50544f2974e27c292302b64 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 19 Mar 2025 21:17:25 +0100 Subject: [PATCH 32/92] updated release notes --- CHANGELOG.md | 1 + options/mzta-release-notes.html | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66335f71..7c3608fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@
  • Added the {%mail_folder_name%} placeholder to get the mail folder name [#253].
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • +
  • Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • Improved the handling of null or undefined placeholders [#288].
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • Simplified Chinese (zh_Hans) translation added, thanks to jeklau.
  • diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 35904484..38fed925 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -12,6 +12,7 @@
  • Added the {%mail_folder_name%} placeholder to get the mail folder name [#253].
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • +
  • Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • Improved the handling of null or undefined placeholders [#288].
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • Simplified Chinese (zh_Hans) translation added, thanks to jeklau.
  • From 9286d15f96160f6b8f649dd24bb85d4f0814e9eb Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 19 Mar 2025 21:23:24 +0100 Subject: [PATCH 33/92] Add loading icon SVG asset --- images/icon-loading.svg | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 images/icon-loading.svg diff --git a/images/icon-loading.svg b/images/icon-loading.svg new file mode 100644 index 00000000..85fab3f3 --- /dev/null +++ b/images/icon-loading.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file From 3a3f64780931a0de3ff2e4a6115bba011b1bbbb5 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 19 Mar 2025 21:36:42 +0100 Subject: [PATCH 34/92] svg loading icon with transparent background. see #295 --- images/icon-loading.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/icon-loading.svg b/images/icon-loading.svg index 85fab3f3..8a95de1a 100644 --- a/images/icon-loading.svg +++ b/images/icon-loading.svg @@ -1,4 +1,4 @@ - + From 8b271722ee6f3f0017ce6e3d2aa105a71c4a9b77 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 19 Mar 2025 21:36:55 +0100 Subject: [PATCH 35/92] Add working status management to handle loading state. see #295 --- js/mzta-working-status.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 js/mzta-working-status.js diff --git a/js/mzta-working-status.js b/js/mzta-working-status.js new file mode 100644 index 00000000..660cea1e --- /dev/null +++ b/js/mzta-working-status.js @@ -0,0 +1,39 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 - 2025 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +export const taWorkingStatus = { + + WorkingLevel: 0, + taLog: console, + + startWorking(){ + this.WorkingLevel++; + this.taLog.log("[startWorking] WorkingLevel: " + this.WorkingLevel); + browser.messageDisplayAction.setIcon({path: "/images/icon-loading.svg"}); + }, + + stopWorking(){ + this.WorkingLevel--; + this.taLog.log("[stopWorking] WorkingLevel: " + this.WorkingLevel); + if(this.WorkingLevel<=0){ + this.WorkingLevel = 0; + browser.messageDisplayAction.setIcon({path: "/images/icon-32px.png"}); + } + } +}; \ No newline at end of file From 1fd58b47888817208070d5501f17fc6b08034a7d Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 19 Mar 2025 21:37:19 +0100 Subject: [PATCH 36/92] Integrate working status management into email automatic and context menu processing flow. see #295 --- mzta-background.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mzta-background.js b/mzta-background.js index 87385ccf..e51417fe 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -25,6 +25,7 @@ import { taPromptUtils } from './js/mzta-utils-prompt.js'; import { mzta_specialCommand } from './js/mzta-special-commands.js'; import { getSpamFilterPrompt } from './js/mzta-prompts.js'; import { taSpamReport } from './js/mzta-spamreport.js'; +import { taWorkingStatus } from './js/mzta-working-status.js'; browser.runtime.onInstalled.addListener(({ reason, previousVersion }) => { // console.log(">>>>>>>>>>> onInstalled: " + JSON.stringify(reason) + ", previousVersion: " + previousVersion); @@ -47,6 +48,7 @@ let prefs_init = {}; await reload_pref_init(); let taLog = new taLogger("mzta-background",prefs_init.do_debug); +taWorkingStatus.taLog = taLog; let special_prompts_ids = getActiveSpecialPromptsIDs(prefs_init.add_tags, await doGetCalendarEvent(prefs_init.get_calendar_event), (prefs_init.connection_type === "chatgpt_web")); @@ -744,6 +746,7 @@ const newEmailListener = (folder, messagesList) => { } async function processEmails(messages, addTagsAuto, spamFilter) { + taWorkingStatus.startWorking(); for await (let message of messages) { let curr_fullMessage = null; let msg_text = null; @@ -824,6 +827,7 @@ async function processEmails(messages, addTagsAuto, spamFilter) { taSpamReport.saveReportData(report_data, message.headerMessageId); } } + taWorkingStatus.stopWorking(); } From f339e2c748f7cbd47d714e79f8a0668d6d4cc497 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 19 Mar 2025 21:46:04 +0100 Subject: [PATCH 37/92] Integrate working status management into mzta_Menus actions. see #295 --- js/mzta-menus.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 9c7fb255..9d50a38d 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -24,6 +24,7 @@ import { taPromptUtils } from './mzta-utils-prompt.js'; import { taLogger } from './mzta-logger.js'; import { placeholdersUtils } from './mzta-placeholders.js'; import { mzta_specialCommand } from './mzta-special-commands.js'; +import { taWorkingStatus } from './mzta-working-status.js'; export class mzta_Menus { @@ -88,6 +89,7 @@ export class mzta_Menus { }; curr_menu_entry.act = async () => { + taWorkingStatus.startWorking(); const tabs = await browser.tabs.query({ active: true, currentWindow: true }); const msg_text = await getMailBody(tabs, placeholdersUtils.hasPlaceholder(curr_prompt.text,'mail_typed_text')); @@ -156,6 +158,7 @@ export class mzta_Menus { let prefs_at = await browser.storage.sync.get({add_tags_maxnum: 3, connection_type: '', add_tags_force_lang: true, default_chatgpt_lang: '', do_debug: false}); if((prefs_at.connection_type === '')||(prefs_at.connection_type === null)||(prefs_at.connection_type === undefined)||(prefs_at.connection_type === 'chatgpt_web')){ console.error("[ThunderAI | AddTags] Invalid connection type: " + prefs_at.connection_type); + taWorkingStatus.stopWorking(); return {ok:'0'}; } fullPrompt = taPromptUtils.finalizePrompt_add_tags(fullPrompt, prefs_at.add_tags_maxnum, prefs_at.add_tags_force_lang, prefs_at.default_chatgpt_lang); @@ -171,11 +174,13 @@ export class mzta_Menus { }catch(err){ console.error("[ThunderAI] Error getting tags: ", err); browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message: "Error getting tags: " + err }); + taWorkingStatus.stopWorking(); return {ok:'0'}; } this.logger.log("tags_current_email: " + tags_current_email); this.logger.log("tags_full_list: " + JSON.stringify(tags_full_list)); browser.tabs.sendMessage(tabs[0].id, {command: "getTags", tags: tags_current_email, messageId: curr_message.id}); + taWorkingStatus.stopWorking(); return {ok:'1'}; break; // Add tags to the email - END } @@ -184,6 +189,7 @@ export class mzta_Menus { let prefs_at = await browser.storage.sync.get({connection_type: ''}); if((prefs_at.connection_type === '')||(prefs_at.connection_type === null)||(prefs_at.connection_type === undefined)||(prefs_at.connection_type === 'chatgpt_web')){ console.error("[ThunderAI | GetCalendarEvent] Invalid connection type: " + prefs_at.connection_type); + taWorkingStatus.stopWorking(); return {ok:'0'}; } /* We expect to receive from the AI a JSON object like this: @@ -203,30 +209,37 @@ export class mzta_Menus { }catch(err){ console.error("[ThunderAI] Error getting calendar event data: ", JSON.stringify(err)); browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage("calendar_getting_data_error") + ": " + JSON.stringify(err) }); + taWorkingStatus.stopWorking(); return {ok:'0'}; } this.logger.log("calendar_event_data: " + calendar_event_data); try{ let result_openCalendarEventDialog = await browser.runtime.sendMessage('thunderai-sparks@micz.it',{action: "openCalendarEventDialog", calendar_event_data: calendar_event_data}) if(result_openCalendarEventDialog == 'ok'){ + taWorkingStatus.stopWorking(); return {ok:'1'}; } else { browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage("calendar_opening_dialog_error") + ": " + result_openCalendarEventDialog.error }); + taWorkingStatus.stopWorking(); return {ok:'0'}; } }catch(err){ console.error("[ThunderAI] Error opening calendar event dialog: ", JSON.stringify(err)); browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage("calendar_opening_dialog_error") + ": " + browser.i18n.getMessage("sparks_not_installed") }); + taWorkingStatus.stopWorking(); return {ok:'0'}; } break; // Get a calendar event info - END } default: console.error("[ThunderAI] Unknown special prompt id: " + curr_prompt.id); + taWorkingStatus.stopWorking(); + return {ok:'0'}; break; } }else{ // Classic prompts for the API webchat this.openChatGPT(fullPrompt, curr_prompt.action, tabs[0].id, curr_prompt.name, curr_prompt.need_custom_text); + taWorkingStatus.stopWorking(); return {ok:'1'}; } }; From 299fc948ce94756077fea4b8650dffeca1ab4903 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 19 Mar 2025 21:47:12 +0100 Subject: [PATCH 38/92] Add compose action icon updates for working status management. see #295 --- js/mzta-working-status.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/mzta-working-status.js b/js/mzta-working-status.js index 660cea1e..9f220d0f 100644 --- a/js/mzta-working-status.js +++ b/js/mzta-working-status.js @@ -26,6 +26,7 @@ export const taWorkingStatus = { this.WorkingLevel++; this.taLog.log("[startWorking] WorkingLevel: " + this.WorkingLevel); browser.messageDisplayAction.setIcon({path: "/images/icon-loading.svg"}); + browser.composeAction.setIcon({path: "/images/icon-loading.svg"}); }, stopWorking(){ @@ -34,6 +35,7 @@ export const taWorkingStatus = { if(this.WorkingLevel<=0){ this.WorkingLevel = 0; browser.messageDisplayAction.setIcon({path: "/images/icon-32px.png"}); + browser.composeAction.setIcon({path: "/images/icon-32px.png"}); } } }; \ No newline at end of file From eea935c8f43af0f14b52d1595f30f7d206f751ff Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 19 Mar 2025 21:48:45 +0100 Subject: [PATCH 39/92] release notes updated --- CHANGELOG.md | 1 + options/mzta-release-notes.html | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c3608fd..3aeffd18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • +
  • The button icon now shows a loading indicator when ThunderAI is performing an operation [#295].
  • Improved the handling of null or undefined placeholders [#288].
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • Simplified Chinese (zh_Hans) translation added, thanks to jeklau.
  • diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 38fed925..4e4c1824 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -13,6 +13,7 @@
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • +
  • The button icon now shows a loading indicator when ThunderAI is performing an operation [#295].
  • Improved the handling of null or undefined placeholders [#288].
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • Simplified Chinese (zh_Hans) translation added, thanks to jeklau.
  • From 4a0d260332b892a5b88c2dc82a3a59d8d208e133 Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 20 Mar 2025 22:11:33 +0100 Subject: [PATCH 40/92] Add timezone preferences for calendar events management. see #250 --- _locales/en/messages.json | 16 +++++++ options/mzta-options-default.js | 2 + .../mzta-get-calendar-event.css | 32 ++++++++++++++ .../mzta-get-calendar-event.html | 44 +++++++++++++++++++ 4 files changed, 94 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6c264095..88b7afde 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -987,6 +987,22 @@ "message": "If checked, an item will be included in the menu to get a calendar event information from selected text.", "description": "" }, + "get_calendar_event_prompt_prefs_title": { + "message": "Calendar Events Options", + "description": "" + }, + "Select_your_timezone": { + "message": "Select your timezone", + "description": "" + }, + "prefs_OptionText_calendar_enforce_timezone": { + "message": "Force the specified timezone", + "description": "" + }, + "prefs_OptionText_calendar_enforce_timezone_Info": { + "message": "If checked, the specified timezone will be enforced for the calendar events.", + "description": "" + }, "prefs_OptionText_btnManageCalendarEventInfo": { "message": "Manage calendar events settings", "description": "" diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index b92a1f66..573d2085 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -52,6 +52,8 @@ export const prefs_default = { add_tags_auto_only_inbox: true, add_tags_context_menu: true, get_calendar_event: true, + calendar_enforce_timezone: false, + calendar_timezone: '', spamfilter: false, spamfilter_threshold: 70, spamfilter_context_menu: true, diff --git a/pages/get-calendar-event/mzta-get-calendar-event.css b/pages/get-calendar-event/mzta-get-calendar-event.css index 75c89f0c..3e9f8137 100644 --- a/pages/get-calendar-event/mzta-get-calendar-event.css +++ b/pages/get-calendar-event/mzta-get-calendar-event.css @@ -18,6 +18,38 @@ justify-content: space-between; } +table#miczPrefs { + padding: 10px; + border-spacing: 0px; + width: 90%; + margin: 0px auto 40px auto; + border: 1px solid #ccc; +} + +.get_calendar_event_table_title{ + width: 90%; + margin: auto; +} + +.section_title{ + font-weight: bold; +} + +table#miczPrefs td{ + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + vertical-align: top; + padding: 2px; +} + +table#miczPrefs tr:first-child td { + border-top: none; +} + +table#miczPrefs tr:last-child td { + border-bottom: none; +} + .autocomplete-container { position: relative; } diff --git a/pages/get-calendar-event/mzta-get-calendar-event.html b/pages/get-calendar-event/mzta-get-calendar-event.html index 8804c8fa..6679dbbb 100644 --- a/pages/get-calendar-event/mzta-get-calendar-event.html +++ b/pages/get-calendar-event/mzta-get-calendar-event.html @@ -11,6 +11,50 @@

    __MSG_GetCalendarEvent_PageTitle__

    __MSG_GetCalendarEvent_info_default__

    +
    __MSG_get_calendar_event_prompt_prefs_title__
    + + + + + +
    __MSG_prefs_OptionText_calendar_enforce_timezone__ + +
    __MSG_GetCalendarEvent_prompt_text_title__
    __MSG_prefs_OptionText_btnManagePrompts_infoline__ __MSG_customPrompts_managePrompts_help__ From 495451c79a3445c9f3764f0078ea87fa252d15ba Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 20 Mar 2025 22:36:38 +0100 Subject: [PATCH 41/92] Enhance calendar event handling with timezone management and JSON extraction. see #250 --- js/mzta-menus.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 9d50a38d..e93bf762 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -19,7 +19,7 @@ // Some original methods are derived from https://github.com/ali-raheem/Aify/blob/cfadf52f576b7be3720b5b73af7c8d3129c054da/plugin/html/actions.js import { getPrompts } from './mzta-prompts.js'; -import { getLanguageDisplayName, getMenuContextCompose, getMenuContextDisplay, i18nConditionalGet, getMailSubject, getTagsList } from './mzta-utils.js' +import { getLanguageDisplayName, getMenuContextCompose, getMenuContextDisplay, i18nConditionalGet, getMailSubject, getTagsList, extractJsonObject } from './mzta-utils.js' import { taPromptUtils } from './mzta-utils-prompt.js'; import { taLogger } from './mzta-logger.js'; import { placeholdersUtils } from './mzta-placeholders.js'; @@ -186,7 +186,7 @@ export class mzta_Menus { } case 'prompt_get_calendar_event': { // Get a calendar event info let calendar_event_data = ''; - let prefs_at = await browser.storage.sync.get({connection_type: ''}); + let prefs_at = await browser.storage.sync.get({connection_type: '', calendar_enforce_timezone: false, calendar_timezone: '',}); if((prefs_at.connection_type === '')||(prefs_at.connection_type === null)||(prefs_at.connection_type === undefined)||(prefs_at.connection_type === 'chatgpt_web')){ console.error("[ThunderAI | GetCalendarEvent] Invalid connection type: " + prefs_at.connection_type); taWorkingStatus.stopWorking(); @@ -212,9 +212,27 @@ export class mzta_Menus { taWorkingStatus.stopWorking(); return {ok:'0'}; } - this.logger.log("calendar_event_data: " + calendar_event_data); + let calendar_event_data_obj = {}; try{ - let result_openCalendarEventDialog = await browser.runtime.sendMessage('thunderai-sparks@micz.it',{action: "openCalendarEventDialog", calendar_event_data: calendar_event_data}) + calendar_event_data_obj = extractJsonObject(calendar_event_data); + }catch(err){ + console.error("[ThunderAI] Error extracting JSON object from calendar event data: ", JSON.stringify(err)); + browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage("calendar_getting_data_error") + ": " + JSON.stringify(err) }); + taWorkingStatus.stopWorking(); + return {ok:'0'}; + } + // Timezone management + calendar_event_data_obj.use_timezone = false; + if(prefs_at.calendar_enforce_timezone){ + calendar_event_data_obj.use_timezone = true; + calendar_event_data_obj.timezone = prefs_at.calendar_timezone; + } + let calendar_event_data_str = JSON.stringify(calendar_event_data_obj); + // Timezone management - END + this.logger.log("calendar_event_data: " + calendar_event_data); + this.logger.log("calendar_event_data_obj: " + calendar_event_data_str); + try{ + let result_openCalendarEventDialog = await browser.runtime.sendMessage('thunderai-sparks@micz.it',{action: "openCalendarEventDialog", calendar_event_data: calendar_event_data_str}) if(result_openCalendarEventDialog == 'ok'){ taWorkingStatus.stopWorking(); return {ok:'1'}; @@ -224,7 +242,7 @@ export class mzta_Menus { return {ok:'0'}; } }catch(err){ - console.error("[ThunderAI] Error opening calendar event dialog: ", JSON.stringify(err)); + console.error("[ThunderAI] Error opening calendar event dialog: ", err); browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage("calendar_opening_dialog_error") + ": " + browser.i18n.getMessage("sparks_not_installed") }); taWorkingStatus.stopWorking(); return {ok:'0'}; From 533a8301afc4fda577ec98b30b65a4fb97a2eea9 Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 20 Mar 2025 22:37:46 +0100 Subject: [PATCH 42/92] Stop working status when no selection is made in mzta_Menus. see #295 --- js/mzta-menus.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/mzta-menus.js b/js/mzta-menus.js index e93bf762..4ec3dcf1 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -98,6 +98,7 @@ export class mzta_Menus { //A selection is needed, but nothing is selected! //alert(browser.i18n.getMessage('prompt_selection_needed')); browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message : browser.i18n.getMessage('prompt_selection_needed') }); + taWorkingStatus.stopWorking(); return {ok:'0'}; } From 67f7450a6f080c3dc9f15528ba9ce592e6a9d433 Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 20 Mar 2025 22:47:37 +0100 Subject: [PATCH 43/92] release notes updated --- CHANGELOG.md | 3 ++- options/mzta-release-notes.html | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aeffd18..a33cc571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@
  • Added the {%mail_folder_name%} placeholder to get the mail folder name [#253].
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • -
  • Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • +
  • [All APIs] Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • +
  • [All APIs] It's now possibile to define a timezone in the calendar event settings page [#250].
  • The button icon now shows a loading indicator when ThunderAI is performing an operation [#295].
  • Improved the handling of null or undefined placeholders [#288].
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 4e4c1824..87eb3e63 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -12,7 +12,8 @@
  • Added the {%mail_folder_name%} placeholder to get the mail folder name [#253].
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • -
  • Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • +
  • [All APIs] Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • +
  • [All APIs] It's now possibile to define a timezone in the calendar event settings page [#250].
  • The button icon now shows a loading indicator when ThunderAI is performing an operation [#295].
  • Improved the handling of null or undefined placeholders [#288].
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • From 689b2426f474f6303e0d785aa156dfb020fb3788 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 20 Mar 2025 23:47:00 +0100 Subject: [PATCH 44/92] Update context menu description to clarify automatic tag addition for spam filtering --- CHANGELOG.md | 2 +- options/mzta-release-notes.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a33cc571..23be88e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@
  • Added the {%mail_folder_name%} placeholder to get the mail folder name [#253].
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • -
  • [All APIs] Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • +
  • [All APIs] Added a context menu to automatically add tags and run the spam filter on selected messages. [#262].
  • [All APIs] It's now possibile to define a timezone in the calendar event settings page [#250].
  • The button icon now shows a loading indicator when ThunderAI is performing an operation [#295].
  • Improved the handling of null or undefined placeholders [#288].
  • diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 87eb3e63..d7aaeb8e 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -12,7 +12,7 @@
  • Added the {%mail_folder_name%} placeholder to get the mail folder name [#253].
  • Added the {%mail_folder_path%} placeholder to get the mail folder path [#253].
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • -
  • [All APIs] Added a context menu to manually add tags and run the spam filter on selected messages. [#262].
  • +
  • [All APIs] Added a context menu to automatically add tags and run the spam filter on selected messages. [#262].
  • [All APIs] It's now possibile to define a timezone in the calendar event settings page [#250].
  • The button icon now shows a loading indicator when ThunderAI is performing an operation [#295].
  • Improved the handling of null or undefined placeholders [#288].
  • From bb530279507fd2e0bb990ad5dd128674c482fdf9 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 20 Mar 2025 23:52:00 +0100 Subject: [PATCH 45/92] Remove debug log for fallback identity in getCurrentIdentity function --- js/mzta-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/mzta-utils.js b/js/mzta-utils.js index decad36c..d2f0e0b5 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -51,7 +51,7 @@ export async function getCurrentIdentity(msgHeader, getFull = false) { identities_array.push({id: identity.id, email:identity.email}) if(fallback_identity === '') { fallback_identity = {id: identity.id, email:identity.email} - console.log(">>>>>>>>>> getCurrentIdentity fallback_identity: " + JSON.stringify(fallback_identity)); + // console.log(">>>>>>>>>> getCurrentIdentity fallback_identity: " + JSON.stringify(fallback_identity)); } } } From c07e4b4cf4c78e818a3ce9d1036c230a4efc4412 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 20 Mar 2025 23:52:00 +0100 Subject: [PATCH 46/92] Add attendees extraction to calendar event generation prompt. see #258 --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 88b7afde..d79bdd1c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -976,7 +976,7 @@ "description": "" }, "prompt_get_calendar_event_full_text": { - "message": "Extract all relevant details required to generate a calendar event from the following text. The extracted information should include:\n- Event Title\n- Start Date and Time (including timezone, if specified)\n- End Date and Time (including timezone, if specified)\n- Full day (if mentioned)\nEnsure the data is formatted clearly and consistently so that it can be directly used for creating a calendar event.\nIf there are relative time references, consider that the date and time of the email are \"{%mail_datetime%}\". Calculate the start date and time based on this reference. If the calculated start date and time are earlier than \"{%current_datetime%}\", recalculate the start date and time using \"{%current_datetime%}\" as the base.\nIf the duration is not specified, set it to one hour.\nIf you're not able to get one or more of the required information, please respond with an empty string.\nGenerate a response in JSON format only. Do not include any additional text or explanations; provide only the JSON. Here is the format to be used:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Calendar event summary here\",\n\"forceAllDay\": false\n}\nHere's the text:\"{%selected_text%}\"", + "message": "Extract all relevant details required to generate a calendar event from the following text. The extracted information should include:\n- Event Title\n- Start Date and Time (including timezone, if specified)\n- End Date and Time (including timezone, if specified)\n- Full day (if mentioned)\n- Attendees\nEnsure the data is formatted clearly and consistently so that it can be directly used for creating a calendar event.\nIf there are relative time references, consider that the date and time of the email are \"{%mail_datetime%}\". Calculate the start date and time based on this reference. If the calculated start date and time are earlier than \"{%current_datetime%}\", recalculate the start date and time using \"{%current_datetime%}\" as the base.\nIf the duration is not specified, set it to one hour.\nThese are the attendees: {%author%}, {%recipients%}, {%cc_list%}. If present, exclude my address: {%account_email_address%}.\nIf you're not able to get one or more of the required information, please respond with an empty string.\nGenerate a response in JSON format only. Do not include any additional text or explanations; provide only the JSON. Here is the format to be used:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Calendar event summary here\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHere's the text:\"{%selected_text%}\"", "description": "" }, "prefs_OptionText_get_calendar_event": { From a1b27fbf4ed08d5a8a0611c233861df155027412 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 20 Mar 2025 23:57:00 +0100 Subject: [PATCH 47/92] release notes updated --- CHANGELOG.md | 1 + options/mzta-release-notes.html | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23be88e3..6949f05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • [All APIs] Added a context menu to automatically add tags and run the spam filter on selected messages. [#262].
  • [All APIs] It's now possibile to define a timezone in the calendar event settings page [#250].
  • +
  • [All APIs] It's now possibile to add the attendees in the calendar event, be sure to update the prompt in the settings [#258].
  • The button icon now shows a loading indicator when ThunderAI is performing an operation [#295].
  • Improved the handling of null or undefined placeholders [#288].
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index d7aaeb8e..293de003 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -14,6 +14,7 @@
  • Added the {%account_email_address%} placeholder to get the current account mail address [#272].
  • [All APIs] Added a context menu to automatically add tags and run the spam filter on selected messages. [#262].
  • [All APIs] It's now possibile to define a timezone in the calendar event settings page [#250].
  • +
  • [All APIs] It's now possibile to add the attendees in the calendar event, be sure to update the prompt in the settings [#258].
  • The button icon now shows a loading indicator when ThunderAI is performing an operation [#295].
  • Improved the handling of null or undefined placeholders [#288].
  • Czech (cs) translation added, thanks to Jaroslav Staněk and Fjuro.
  • From e95c28ab64c5d3d6a1dc6b0d3a186bbfc6e53c52 Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 21 Mar 2025 00:02:00 +0100 Subject: [PATCH 48/92] Add error message for no active calendar and improve error handling in calendar event dialog --- _locales/en/messages.json | 4 ++++ js/mzta-menus.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d79bdd1c..90513981 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1206,5 +1206,9 @@ "prefs_OptionText_spamfilter_context_menu_Info": { "message": "If checked, the \"Analyze for spam\" context menu item will be shown when right-clicking on an email in the messages list.", "description": "" + }, + "noActiveCalendar": { + "message": "No active calendar found!", + "description": "" } } \ No newline at end of file diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 4ec3dcf1..36f236cd 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -238,6 +238,9 @@ export class mzta_Menus { taWorkingStatus.stopWorking(); return {ok:'1'}; } else { + if (result_openCalendarEventDialog.error.startsWith("|>>")) { + result_openCalendarEventDialog.error = browser.i18n.getMessage(result_openCalendarEventDialog.error.substring(3)); + } browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage("calendar_opening_dialog_error") + ": " + result_openCalendarEventDialog.error }); taWorkingStatus.stopWorking(); return {ok:'0'}; From dc8fb0b79235efbddac91a7f8cbe2e174e088ce6 Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 21 Mar 2025 00:07:00 +0100 Subject: [PATCH 49/92] Add use_diff_viewer attribute to prompt configurations. see #109 --- js/mzta-prompts.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/js/mzta-prompts.js b/js/mzta-prompts.js index 6ed4bad4..3873708f 100644 --- a/js/mzta-prompts.js +++ b/js/mzta-prompts.js @@ -45,8 +45,12 @@ 1: Custom text needed Define response language (define_response_lang attribute): - 0: Do not include a statement about the response language in the prompt. - 1: Include a statement about the response language in the prompt. + 0: Do not include a statement about the response language in the prompt + 1: Include a statement about the response language in the prompt + + Use the diff viewer (use_diff_viewer attribute): + 0: Do not use the diff viewer + 1: Use the diff viewer ================ USER PROPERTIES Enabled (enabled attribute): @@ -72,6 +76,7 @@ const defaultPrompts = [ need_signature: "1", need_custom_text: "0", define_response_lang: "1", + use_diff_viewer: "0", is_default: "1", is_special: "0", }, @@ -85,6 +90,7 @@ const defaultPrompts = [ need_signature: "1", need_custom_text: "0", define_response_lang: "1", + use_diff_viewer: "0", is_default: "1", is_special: "0", }, @@ -98,6 +104,7 @@ const defaultPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "1", + use_diff_viewer: "1", is_default: "1", is_special: "0", }, @@ -111,6 +118,7 @@ const defaultPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "1", + use_diff_viewer: "1", is_default: "1", is_special: "0", }, @@ -124,6 +132,7 @@ const defaultPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "1", + use_diff_viewer: "0", is_default: "1", is_special: "0", }, @@ -137,6 +146,7 @@ const defaultPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "1", + use_diff_viewer: "0", is_default: "1", is_special: "0", }, @@ -150,6 +160,7 @@ const defaultPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "0", + use_diff_viewer: "0", is_default: "1", is_special: "0", }, @@ -163,6 +174,7 @@ const defaultPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "0", + use_diff_viewer: "0", is_default: "1", is_special: "0", }, @@ -179,6 +191,7 @@ const specialPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "0", + use_diff_viewer: "0", is_default: "1", is_special: "1", }, @@ -192,6 +205,7 @@ const specialPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "0", + use_diff_viewer: "0", is_default: "1", is_special: "1", }, @@ -205,6 +219,7 @@ const specialPrompts = [ need_signature: "0", need_custom_text: "0", define_response_lang: "0", + use_diff_viewer: "0", is_default: "1", is_special: "1", }, From 99c70df10c09e8e2345a8d93a8eae094bd924262 Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 21 Mar 2025 00:12:00 +0100 Subject: [PATCH 50/92] Set default value for use_diff_viewer in custom prompts if undefined. see #109 --- js/mzta-prompts.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/js/mzta-prompts.js b/js/mzta-prompts.js index 3873708f..9f1659d9 100644 --- a/js/mzta-prompts.js +++ b/js/mzta-prompts.js @@ -339,6 +339,11 @@ async function getCustomPrompts() { if(prefs._custom_prompt === null){ return []; } else { + prefs._custom_prompt.forEach(prompt => { + if (prompt.use_diff_viewer === undefined) { + prompt.use_diff_viewer = "0"; + } + }); return prefs._custom_prompt; } } From 9367e35280f176dfef9967ed41b303f53416dcb1 Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 21 Mar 2025 00:12:00 +0100 Subject: [PATCH 51/92] Add 'Enable the diff viewer' option to custom prompts see #109 --- _locales/en/messages.json | 4 ++++ pages/customprompts/mzta-custom-prompts.html | 2 ++ pages/customprompts/mzta-custom-prompts.js | 9 ++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 90513981..b654ddf6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -131,6 +131,10 @@ "message": "Enabled", "description": "" }, + "customPrompts_form_label_use_diff_viewer": { + "message": "Enable the diff viewer", + "description": "" + }, "customPrompts_form_required_fields": { "message": "Required fields", "description": "" diff --git a/pages/customprompts/mzta-custom-prompts.html b/pages/customprompts/mzta-custom-prompts.html index fae1d1a3..4a8c0424 100644 --- a/pages/customprompts/mzta-custom-prompts.html +++ b/pages/customprompts/mzta-custom-prompts.html @@ -57,6 +57,8 @@
    +
    + diff --git a/pages/customprompts/mzta-custom-prompts.js b/pages/customprompts/mzta-custom-prompts.js index 351b1869..ac4f8f75 100644 --- a/pages/customprompts/mzta-custom-prompts.js +++ b/pages/customprompts/mzta-custom-prompts.js @@ -155,6 +155,7 @@ document.addEventListener('DOMContentLoaded', async () => { var checkboxNeedSignatureNew = document.getElementById('checkboxNeedSignatureNew'); var checkboxNeedCustomTextNew = document.getElementById('checkboxNeedCustomTextNew'); var checkboxDefineResponseLangNew = document.getElementById('checkboxDefineResponseLangNew'); + var checkboxUseDiffViewerNew = document.getElementById('checkboxUseDiffViewerNew'); const btnAddNew = document.getElementById('btnAddNew'); btnAddNew.addEventListener('click', (e) => { @@ -172,6 +173,7 @@ document.addEventListener('DOMContentLoaded', async () => { need_signature: (checkboxNeedSignatureNew.checked) ? 1 : 0, need_custom_text: (checkboxNeedCustomTextNew.checked) ? 1 : 0, define_response_lang: (checkboxDefineResponseLangNew.checked) ? 1 : 0, + use_diff_viewer: (checkboxUseDiffViewerNew.checked) ? 1 : 0, enabled: 1, position_compose: positionMax_compose + 1, position_display: positionMax_display + 1, @@ -318,6 +320,7 @@ function showItemRowEditor(tr) { tr.querySelector('input.need_signature').disabled = false; tr.querySelector('input.need_custom_text').disabled = false; tr.querySelector('input.define_response_lang').disabled = false; + tr.querySelector('input.use_diff_viewer').disabled = false; } function hideItemRowEditor(tr) { @@ -335,6 +338,7 @@ function hideItemRowEditor(tr) { tr.querySelector('input.need_signature').disabled = true; tr.querySelector('input.need_custom_text').disabled = true; tr.querySelector('input.define_response_lang').disabled = true; + tr.querySelector('input.use_diff_viewer').disabled = true; } // Confirm and log deletion action @@ -413,7 +417,7 @@ function handleInputChange(e) { function loadPromptsList(values){ // console.log('>>>>>>>> loadPromptsList values: ' + JSON.stringify(values)); let options = { - valueNames: [ { data: ['idnum'] }, 'is_default', 'id', 'name', 'text', 'type', 'action', 'position_compose', 'position_display', { name: 'need_selected', attr: 'checked_val'}, { name: 'need_signature', attr: 'checked_val'}, { name: 'need_custom_text', attr: 'checked_val'}, { name: 'define_response_lang', attr: 'checked_val'}, { name: 'enabled', attr: 'checked_val'} ], + valueNames: [ { data: ['idnum'] }, 'is_default', 'id', 'name', 'text', 'type', 'action', 'position_compose', 'position_display', { name: 'need_selected', attr: 'checked_val'}, { name: 'need_signature', attr: 'checked_val'}, { name: 'need_custom_text', attr: 'checked_val'}, { name: 'define_response_lang', attr: 'checked_val'}, { name: 'use_diff_viewer', attr: 'checked_val'}, { name: 'enabled', attr: 'checked_val'} ], item: function(values) { let type_output = ''; switch(String(values.type)){ @@ -471,6 +475,8 @@ function loadPromptsList(values){
    __MSG_customPrompts_form_label_define_response_lang__
    + __MSG_customPrompts_form_label_use_diff_viewer__ +
    __MSG_customPrompts_form_label_enabled__ @@ -609,6 +615,7 @@ function checkSelectedBoxes(checkboxes = null) { ...document.querySelectorAll('.need_signature[type="checkbox"]'), ...document.querySelectorAll('.need_custom_text[type="checkbox"]'), ...document.querySelectorAll('.define_response_lang[type="checkbox"]'), + ...document.querySelectorAll('.use_diff_viewer[type="checkbox"]'), ...document.querySelectorAll('.enabled[type="checkbox"]'), ]; } From 645d04a3e5d2714e269ce7f64b6056c371c93cff Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 21 Mar 2025 00:17:00 +0100 Subject: [PATCH 52/92] diff lib added. see #109 --- js/lib/diff.js | 2071 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2071 insertions(+) create mode 100644 js/lib/diff.js diff --git a/js/lib/diff.js b/js/lib/diff.js new file mode 100644 index 00000000..d2e82e34 --- /dev/null +++ b/js/lib/diff.js @@ -0,0 +1,2071 @@ +/* https://www.npmjs.com/package/diff */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Diff = {})); +})(this, (function (exports) { 'use strict'; + + function Diff() {} + Diff.prototype = { + diff: function diff(oldString, newString) { + var _options$timeout; + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var callback = options.callback; + if (typeof options === 'function') { + callback = options; + options = {}; + } + var self = this; + function done(value) { + value = self.postProcess(value, options); + if (callback) { + setTimeout(function () { + callback(value); + }, 0); + return true; + } else { + return value; + } + } + + // Allow subclasses to massage the input prior to running + oldString = this.castInput(oldString, options); + newString = this.castInput(newString, options); + oldString = this.removeEmpty(this.tokenize(oldString, options)); + newString = this.removeEmpty(this.tokenize(newString, options)); + var newLen = newString.length, + oldLen = oldString.length; + var editLength = 1; + var maxEditLength = newLen + oldLen; + if (options.maxEditLength != null) { + maxEditLength = Math.min(maxEditLength, options.maxEditLength); + } + var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity; + var abortAfterTimestamp = Date.now() + maxExecutionTime; + var bestPath = [{ + oldPos: -1, + lastComponent: undefined + }]; + + // Seed editLength = 0, i.e. the content starts with the same values + var newPos = this.extractCommon(bestPath[0], newString, oldString, 0, options); + if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) { + // Identity per the equality and tokenizer + return done(buildValues(self, bestPath[0].lastComponent, newString, oldString, self.useLongestToken)); + } + + // Once we hit the right edge of the edit graph on some diagonal k, we can + // definitely reach the end of the edit graph in no more than k edits, so + // there's no point in considering any moves to diagonal k+1 any more (from + // which we're guaranteed to need at least k+1 more edits). + // Similarly, once we've reached the bottom of the edit graph, there's no + // point considering moves to lower diagonals. + // We record this fact by setting minDiagonalToConsider and + // maxDiagonalToConsider to some finite value once we've hit the edge of + // the edit graph. + // This optimization is not faithful to the original algorithm presented in + // Myers's paper, which instead pointlessly extends D-paths off the end of + // the edit graph - see page 7 of Myers's paper which notes this point + // explicitly and illustrates it with a diagram. This has major performance + // implications for some common scenarios. For instance, to compute a diff + // where the new text simply appends d characters on the end of the + // original text of length n, the true Myers algorithm will take O(n+d^2) + // time while this optimization needs only O(n+d) time. + var minDiagonalToConsider = -Infinity, + maxDiagonalToConsider = Infinity; + + // Main worker method. checks all permutations of a given edit length for acceptance. + function execEditLength() { + for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) { + var basePath = void 0; + var removePath = bestPath[diagonalPath - 1], + addPath = bestPath[diagonalPath + 1]; + if (removePath) { + // No one else is going to attempt to use this value, clear it + bestPath[diagonalPath - 1] = undefined; + } + var canAdd = false; + if (addPath) { + // what newPos will be after we do an insertion: + var addPathNewPos = addPath.oldPos - diagonalPath; + canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen; + } + var canRemove = removePath && removePath.oldPos + 1 < oldLen; + if (!canAdd && !canRemove) { + // If this path is a terminal then prune + bestPath[diagonalPath] = undefined; + continue; + } + + // Select the diagonal that we want to branch from. We select the prior + // path whose position in the old string is the farthest from the origin + // and does not pass the bounds of the diff graph + if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) { + basePath = self.addToPath(addPath, true, false, 0, options); + } else { + basePath = self.addToPath(removePath, false, true, 1, options); + } + newPos = self.extractCommon(basePath, newString, oldString, diagonalPath, options); + if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) { + // If we have hit the end of both strings, then we are done + return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken)); + } else { + bestPath[diagonalPath] = basePath; + if (basePath.oldPos + 1 >= oldLen) { + maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1); + } + if (newPos + 1 >= newLen) { + minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1); + } + } + } + editLength++; + } + + // Performs the length of edit iteration. Is a bit fugly as this has to support the + // sync and async mode which is never fun. Loops over execEditLength until a value + // is produced, or until the edit length exceeds options.maxEditLength (if given), + // in which case it will return undefined. + if (callback) { + (function exec() { + setTimeout(function () { + if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) { + return callback(); + } + if (!execEditLength()) { + exec(); + } + }, 0); + })(); + } else { + while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) { + var ret = execEditLength(); + if (ret) { + return ret; + } + } + } + }, + addToPath: function addToPath(path, added, removed, oldPosInc, options) { + var last = path.lastComponent; + if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) { + return { + oldPos: path.oldPos + oldPosInc, + lastComponent: { + count: last.count + 1, + added: added, + removed: removed, + previousComponent: last.previousComponent + } + }; + } else { + return { + oldPos: path.oldPos + oldPosInc, + lastComponent: { + count: 1, + added: added, + removed: removed, + previousComponent: last + } + }; + } + }, + extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath, options) { + var newLen = newString.length, + oldLen = oldString.length, + oldPos = basePath.oldPos, + newPos = oldPos - diagonalPath, + commonCount = 0; + while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldString[oldPos + 1], newString[newPos + 1], options)) { + newPos++; + oldPos++; + commonCount++; + if (options.oneChangePerToken) { + basePath.lastComponent = { + count: 1, + previousComponent: basePath.lastComponent, + added: false, + removed: false + }; + } + } + if (commonCount && !options.oneChangePerToken) { + basePath.lastComponent = { + count: commonCount, + previousComponent: basePath.lastComponent, + added: false, + removed: false + }; + } + basePath.oldPos = oldPos; + return newPos; + }, + equals: function equals(left, right, options) { + if (options.comparator) { + return options.comparator(left, right); + } else { + return left === right || options.ignoreCase && left.toLowerCase() === right.toLowerCase(); + } + }, + removeEmpty: function removeEmpty(array) { + var ret = []; + for (var i = 0; i < array.length; i++) { + if (array[i]) { + ret.push(array[i]); + } + } + return ret; + }, + castInput: function castInput(value) { + return value; + }, + tokenize: function tokenize(value) { + return Array.from(value); + }, + join: function join(chars) { + return chars.join(''); + }, + postProcess: function postProcess(changeObjects) { + return changeObjects; + } + }; + function buildValues(diff, lastComponent, newString, oldString, useLongestToken) { + // First we convert our linked list of components in reverse order to an + // array in the right order: + var components = []; + var nextComponent; + while (lastComponent) { + components.push(lastComponent); + nextComponent = lastComponent.previousComponent; + delete lastComponent.previousComponent; + lastComponent = nextComponent; + } + components.reverse(); + var componentPos = 0, + componentLen = components.length, + newPos = 0, + oldPos = 0; + for (; componentPos < componentLen; componentPos++) { + var component = components[componentPos]; + if (!component.removed) { + if (!component.added && useLongestToken) { + var value = newString.slice(newPos, newPos + component.count); + value = value.map(function (value, i) { + var oldValue = oldString[oldPos + i]; + return oldValue.length > value.length ? oldValue : value; + }); + component.value = diff.join(value); + } else { + component.value = diff.join(newString.slice(newPos, newPos + component.count)); + } + newPos += component.count; + + // Common case + if (!component.added) { + oldPos += component.count; + } + } else { + component.value = diff.join(oldString.slice(oldPos, oldPos + component.count)); + oldPos += component.count; + } + } + return components; + } + + var characterDiff = new Diff(); + function diffChars(oldStr, newStr, options) { + return characterDiff.diff(oldStr, newStr, options); + } + + function longestCommonPrefix(str1, str2) { + var i; + for (i = 0; i < str1.length && i < str2.length; i++) { + if (str1[i] != str2[i]) { + return str1.slice(0, i); + } + } + return str1.slice(0, i); + } + function longestCommonSuffix(str1, str2) { + var i; + + // Unlike longestCommonPrefix, we need a special case to handle all scenarios + // where we return the empty string since str1.slice(-0) will return the + // entire string. + if (!str1 || !str2 || str1[str1.length - 1] != str2[str2.length - 1]) { + return ''; + } + for (i = 0; i < str1.length && i < str2.length; i++) { + if (str1[str1.length - (i + 1)] != str2[str2.length - (i + 1)]) { + return str1.slice(-i); + } + } + return str1.slice(-i); + } + function replacePrefix(string, oldPrefix, newPrefix) { + if (string.slice(0, oldPrefix.length) != oldPrefix) { + throw Error("string ".concat(JSON.stringify(string), " doesn't start with prefix ").concat(JSON.stringify(oldPrefix), "; this is a bug")); + } + return newPrefix + string.slice(oldPrefix.length); + } + function replaceSuffix(string, oldSuffix, newSuffix) { + if (!oldSuffix) { + return string + newSuffix; + } + if (string.slice(-oldSuffix.length) != oldSuffix) { + throw Error("string ".concat(JSON.stringify(string), " doesn't end with suffix ").concat(JSON.stringify(oldSuffix), "; this is a bug")); + } + return string.slice(0, -oldSuffix.length) + newSuffix; + } + function removePrefix(string, oldPrefix) { + return replacePrefix(string, oldPrefix, ''); + } + function removeSuffix(string, oldSuffix) { + return replaceSuffix(string, oldSuffix, ''); + } + function maximumOverlap(string1, string2) { + return string2.slice(0, overlapCount(string1, string2)); + } + + // Nicked from https://stackoverflow.com/a/60422853/1709587 + function overlapCount(a, b) { + // Deal with cases where the strings differ in length + var startA = 0; + if (a.length > b.length) { + startA = a.length - b.length; + } + var endB = b.length; + if (a.length < b.length) { + endB = a.length; + } + // Create a back-reference for each index + // that should be followed in case of a mismatch. + // We only need B to make these references: + var map = Array(endB); + var k = 0; // Index that lags behind j + map[0] = 0; + for (var j = 1; j < endB; j++) { + if (b[j] == b[k]) { + map[j] = map[k]; // skip over the same character (optional optimisation) + } else { + map[j] = k; + } + while (k > 0 && b[j] != b[k]) { + k = map[k]; + } + if (b[j] == b[k]) { + k++; + } + } + // Phase 2: use these references while iterating over A + k = 0; + for (var i = startA; i < a.length; i++) { + while (k > 0 && a[i] != b[k]) { + k = map[k]; + } + if (a[i] == b[k]) { + k++; + } + } + return k; + } + + /** + * Returns true if the string consistently uses Windows line endings. + */ + function hasOnlyWinLineEndings(string) { + return string.includes('\r\n') && !string.startsWith('\n') && !string.match(/[^\r]\n/); + } + + /** + * Returns true if the string consistently uses Unix line endings. + */ + function hasOnlyUnixLineEndings(string) { + return !string.includes('\r\n') && string.includes('\n'); + } + + // Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode + // + // Ranges and exceptions: + // Latin-1 Supplement, 0080–00FF + // - U+00D7 × Multiplication sign + // - U+00F7 ÷ Division sign + // Latin Extended-A, 0100–017F + // Latin Extended-B, 0180–024F + // IPA Extensions, 0250–02AF + // Spacing Modifier Letters, 02B0–02FF + // - U+02C7 ˇ ˇ Caron + // - U+02D8 ˘ ˘ Breve + // - U+02D9 ˙ ˙ Dot Above + // - U+02DA ˚ ˚ Ring Above + // - U+02DB ˛ ˛ Ogonek + // - U+02DC ˜ ˜ Small Tilde + // - U+02DD ˝ ˝ Double Acute Accent + // Latin Extended Additional, 1E00–1EFF + var extendedWordChars = "a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}"; + + // Each token is one of the following: + // - A punctuation mark plus the surrounding whitespace + // - A word plus the surrounding whitespace + // - Pure whitespace (but only in the special case where this the entire text + // is just whitespace) + // + // We have to include surrounding whitespace in the tokens because the two + // alternative approaches produce horribly broken results: + // * If we just discard the whitespace, we can't fully reproduce the original + // text from the sequence of tokens and any attempt to render the diff will + // get the whitespace wrong. + // * If we have separate tokens for whitespace, then in a typical text every + // second token will be a single space character. But this often results in + // the optimal diff between two texts being a perverse one that preserves + // the spaces between words but deletes and reinserts actual common words. + // See https://github.com/kpdecker/jsdiff/issues/160#issuecomment-1866099640 + // for an example. + // + // Keeping the surrounding whitespace of course has implications for .equals + // and .join, not just .tokenize. + + // This regex does NOT fully implement the tokenization rules described above. + // Instead, it gives runs of whitespace their own "token". The tokenize method + // then handles stitching whitespace tokens onto adjacent word or punctuation + // tokens. + var tokenizeIncludingWhitespace = new RegExp("[".concat(extendedWordChars, "]+|\\s+|[^").concat(extendedWordChars, "]"), 'ug'); + var wordDiff = new Diff(); + wordDiff.equals = function (left, right, options) { + if (options.ignoreCase) { + left = left.toLowerCase(); + right = right.toLowerCase(); + } + return left.trim() === right.trim(); + }; + wordDiff.tokenize = function (value) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var parts; + if (options.intlSegmenter) { + if (options.intlSegmenter.resolvedOptions().granularity != 'word') { + throw new Error('The segmenter passed must have a granularity of "word"'); + } + parts = Array.from(options.intlSegmenter.segment(value), function (segment) { + return segment.segment; + }); + } else { + parts = value.match(tokenizeIncludingWhitespace) || []; + } + var tokens = []; + var prevPart = null; + parts.forEach(function (part) { + if (/\s/.test(part)) { + if (prevPart == null) { + tokens.push(part); + } else { + tokens.push(tokens.pop() + part); + } + } else if (/\s/.test(prevPart)) { + if (tokens[tokens.length - 1] == prevPart) { + tokens.push(tokens.pop() + part); + } else { + tokens.push(prevPart + part); + } + } else { + tokens.push(part); + } + prevPart = part; + }); + return tokens; + }; + wordDiff.join = function (tokens) { + // Tokens being joined here will always have appeared consecutively in the + // same text, so we can simply strip off the leading whitespace from all the + // tokens except the first (and except any whitespace-only tokens - but such + // a token will always be the first and only token anyway) and then join them + // and the whitespace around words and punctuation will end up correct. + return tokens.map(function (token, i) { + if (i == 0) { + return token; + } else { + return token.replace(/^\s+/, ''); + } + }).join(''); + }; + wordDiff.postProcess = function (changes, options) { + if (!changes || options.oneChangePerToken) { + return changes; + } + var lastKeep = null; + // Change objects representing any insertion or deletion since the last + // "keep" change object. There can be at most one of each. + var insertion = null; + var deletion = null; + changes.forEach(function (change) { + if (change.added) { + insertion = change; + } else if (change.removed) { + deletion = change; + } else { + if (insertion || deletion) { + // May be false at start of text + dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change); + } + lastKeep = change; + insertion = null; + deletion = null; + } + }); + if (insertion || deletion) { + dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null); + } + return changes; + }; + function diffWords(oldStr, newStr, options) { + // This option has never been documented and never will be (it's clearer to + // just call `diffWordsWithSpace` directly if you need that behavior), but + // has existed in jsdiff for a long time, so we retain support for it here + // for the sake of backwards compatibility. + if ((options === null || options === void 0 ? void 0 : options.ignoreWhitespace) != null && !options.ignoreWhitespace) { + return diffWordsWithSpace(oldStr, newStr, options); + } + return wordDiff.diff(oldStr, newStr, options); + } + function dedupeWhitespaceInChangeObjects(startKeep, deletion, insertion, endKeep) { + // Before returning, we tidy up the leading and trailing whitespace of the + // change objects to eliminate cases where trailing whitespace in one object + // is repeated as leading whitespace in the next. + // Below are examples of the outcomes we want here to explain the code. + // I=insert, K=keep, D=delete + // 1. diffing 'foo bar baz' vs 'foo baz' + // Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz' + // After cleanup, we want: K:'foo ' D:'bar ' K:'baz' + // + // 2. Diffing 'foo bar baz' vs 'foo qux baz' + // Prior to cleanup, we have K:'foo ' D:' bar ' I:' qux ' K:' baz' + // After cleanup, we want K:'foo ' D:'bar' I:'qux' K:' baz' + // + // 3. Diffing 'foo\nbar baz' vs 'foo baz' + // Prior to cleanup, we have K:'foo ' D:'\nbar ' K:' baz' + // After cleanup, we want K'foo' D:'\nbar' K:' baz' + // + // 4. Diffing 'foo baz' vs 'foo\nbar baz' + // Prior to cleanup, we have K:'foo\n' I:'\nbar ' K:' baz' + // After cleanup, we ideally want K'foo' I:'\nbar' K:' baz' + // but don't actually manage this currently (the pre-cleanup change + // objects don't contain enough information to make it possible). + // + // 5. Diffing 'foo bar baz' vs 'foo baz' + // Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz' + // After cleanup, we want K:'foo ' D:' bar ' K:'baz' + // + // Our handling is unavoidably imperfect in the case where there's a single + // indel between keeps and the whitespace has changed. For instance, consider + // diffing 'foo\tbar\nbaz' vs 'foo baz'. Unless we create an extra change + // object to represent the insertion of the space character (which isn't even + // a token), we have no way to avoid losing information about the texts' + // original whitespace in the result we return. Still, we do our best to + // output something that will look sensible if we e.g. print it with + // insertions in green and deletions in red. + + // Between two "keep" change objects (or before the first or after the last + // change object), we can have either: + // * A "delete" followed by an "insert" + // * Just an "insert" + // * Just a "delete" + // We handle the three cases separately. + if (deletion && insertion) { + var oldWsPrefix = deletion.value.match(/^\s*/)[0]; + var oldWsSuffix = deletion.value.match(/\s*$/)[0]; + var newWsPrefix = insertion.value.match(/^\s*/)[0]; + var newWsSuffix = insertion.value.match(/\s*$/)[0]; + if (startKeep) { + var commonWsPrefix = longestCommonPrefix(oldWsPrefix, newWsPrefix); + startKeep.value = replaceSuffix(startKeep.value, newWsPrefix, commonWsPrefix); + deletion.value = removePrefix(deletion.value, commonWsPrefix); + insertion.value = removePrefix(insertion.value, commonWsPrefix); + } + if (endKeep) { + var commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix); + endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix); + deletion.value = removeSuffix(deletion.value, commonWsSuffix); + insertion.value = removeSuffix(insertion.value, commonWsSuffix); + } + } else if (insertion) { + // The whitespaces all reflect what was in the new text rather than + // the old, so we essentially have no information about whitespace + // insertion or deletion. We just want to dedupe the whitespace. + // We do that by having each change object keep its trailing + // whitespace and deleting duplicate leading whitespace where + // present. + if (startKeep) { + insertion.value = insertion.value.replace(/^\s*/, ''); + } + if (endKeep) { + endKeep.value = endKeep.value.replace(/^\s*/, ''); + } + // otherwise we've got a deletion and no insertion + } else if (startKeep && endKeep) { + var newWsFull = endKeep.value.match(/^\s*/)[0], + delWsStart = deletion.value.match(/^\s*/)[0], + delWsEnd = deletion.value.match(/\s*$/)[0]; + + // Any whitespace that comes straight after startKeep in both the old and + // new texts, assign to startKeep and remove from the deletion. + var newWsStart = longestCommonPrefix(newWsFull, delWsStart); + deletion.value = removePrefix(deletion.value, newWsStart); + + // Any whitespace that comes straight before endKeep in both the old and + // new texts, and hasn't already been assigned to startKeep, assign to + // endKeep and remove from the deletion. + var newWsEnd = longestCommonSuffix(removePrefix(newWsFull, newWsStart), delWsEnd); + deletion.value = removeSuffix(deletion.value, newWsEnd); + endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd); + + // If there's any whitespace from the new text that HASN'T already been + // assigned, assign it to the start: + startKeep.value = replaceSuffix(startKeep.value, newWsFull, newWsFull.slice(0, newWsFull.length - newWsEnd.length)); + } else if (endKeep) { + // We are at the start of the text. Preserve all the whitespace on + // endKeep, and just remove whitespace from the end of deletion to the + // extent that it overlaps with the start of endKeep. + var endKeepWsPrefix = endKeep.value.match(/^\s*/)[0]; + var deletionWsSuffix = deletion.value.match(/\s*$/)[0]; + var overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix); + deletion.value = removeSuffix(deletion.value, overlap); + } else if (startKeep) { + // We are at the END of the text. Preserve all the whitespace on + // startKeep, and just remove whitespace from the start of deletion to + // the extent that it overlaps with the end of startKeep. + var startKeepWsSuffix = startKeep.value.match(/\s*$/)[0]; + var deletionWsPrefix = deletion.value.match(/^\s*/)[0]; + var _overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix); + deletion.value = removePrefix(deletion.value, _overlap); + } + } + var wordWithSpaceDiff = new Diff(); + wordWithSpaceDiff.tokenize = function (value) { + // Slightly different to the tokenizeIncludingWhitespace regex used above in + // that this one treats each individual newline as a distinct tokens, rather + // than merging them into other surrounding whitespace. This was requested + // in https://github.com/kpdecker/jsdiff/issues/180 & + // https://github.com/kpdecker/jsdiff/issues/211 + var regex = new RegExp("(\\r?\\n)|[".concat(extendedWordChars, "]+|[^\\S\\n\\r]+|[^").concat(extendedWordChars, "]"), 'ug'); + return value.match(regex) || []; + }; + function diffWordsWithSpace(oldStr, newStr, options) { + return wordWithSpaceDiff.diff(oldStr, newStr, options); + } + + function generateOptions(options, defaults) { + if (typeof options === 'function') { + defaults.callback = options; + } else if (options) { + for (var name in options) { + /* istanbul ignore else */ + if (options.hasOwnProperty(name)) { + defaults[name] = options[name]; + } + } + } + return defaults; + } + + var lineDiff = new Diff(); + lineDiff.tokenize = function (value, options) { + if (options.stripTrailingCr) { + // remove one \r before \n to match GNU diff's --strip-trailing-cr behavior + value = value.replace(/\r\n/g, '\n'); + } + var retLines = [], + linesAndNewlines = value.split(/(\n|\r\n)/); + + // Ignore the final empty token that occurs if the string ends with a new line + if (!linesAndNewlines[linesAndNewlines.length - 1]) { + linesAndNewlines.pop(); + } + + // Merge the content and line separators into single tokens + for (var i = 0; i < linesAndNewlines.length; i++) { + var line = linesAndNewlines[i]; + if (i % 2 && !options.newlineIsToken) { + retLines[retLines.length - 1] += line; + } else { + retLines.push(line); + } + } + return retLines; + }; + lineDiff.equals = function (left, right, options) { + // If we're ignoring whitespace, we need to normalise lines by stripping + // whitespace before checking equality. (This has an annoying interaction + // with newlineIsToken that requires special handling: if newlines get their + // own token, then we DON'T want to trim the *newline* tokens down to empty + // strings, since this would cause us to treat whitespace-only line content + // as equal to a separator between lines, which would be weird and + // inconsistent with the documented behavior of the options.) + if (options.ignoreWhitespace) { + if (!options.newlineIsToken || !left.includes('\n')) { + left = left.trim(); + } + if (!options.newlineIsToken || !right.includes('\n')) { + right = right.trim(); + } + } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) { + if (left.endsWith('\n')) { + left = left.slice(0, -1); + } + if (right.endsWith('\n')) { + right = right.slice(0, -1); + } + } + return Diff.prototype.equals.call(this, left, right, options); + }; + function diffLines(oldStr, newStr, callback) { + return lineDiff.diff(oldStr, newStr, callback); + } + + // Kept for backwards compatibility. This is a rather arbitrary wrapper method + // that just calls `diffLines` with `ignoreWhitespace: true`. It's confusing to + // have two ways to do exactly the same thing in the API, so we no longer + // document this one (library users should explicitly use `diffLines` with + // `ignoreWhitespace: true` instead) but we keep it around to maintain + // compatibility with code that used old versions. + function diffTrimmedLines(oldStr, newStr, callback) { + var options = generateOptions(callback, { + ignoreWhitespace: true + }); + return lineDiff.diff(oldStr, newStr, options); + } + + var sentenceDiff = new Diff(); + sentenceDiff.tokenize = function (value) { + return value.split(/(\S.+?[.!?])(?=\s+|$)/); + }; + function diffSentences(oldStr, newStr, callback) { + return sentenceDiff.diff(oldStr, newStr, callback); + } + + var cssDiff = new Diff(); + cssDiff.tokenize = function (value) { + return value.split(/([{}:;,]|\s+)/); + }; + function diffCss(oldStr, newStr, callback) { + return cssDiff.diff(oldStr, newStr, callback); + } + + function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && (o = o.filter(function (r) { + return Object.getOwnPropertyDescriptor(e, r).enumerable; + })), t.push.apply(t, o); + } + return t; + } + function _objectSpread2(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { + _defineProperty(e, r, t[r]); + }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { + Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); + }); + } + return e; + } + function _toPrimitive(t, r) { + if ("object" != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || "default"); + if ("object" != typeof i) return i; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return ("string" === r ? String : Number)(t); + } + function _toPropertyKey(t) { + var i = _toPrimitive(t, "string"); + return "symbol" == typeof i ? i : i + ""; + } + function _typeof(o) { + "@babel/helpers - typeof"; + + return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { + return typeof o; + } : function (o) { + return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; + }, _typeof(o); + } + function _defineProperty(obj, key, value) { + key = _toPropertyKey(key); + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + return obj; + } + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); + } + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) return _arrayLikeToArray(arr); + } + function _iterableToArray(iter) { + if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); + } + function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); + } + function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + return arr2; + } + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + var jsonDiff = new Diff(); + // Discriminate between two lines of pretty-printed, serialized JSON where one of them has a + // dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output: + jsonDiff.useLongestToken = true; + jsonDiff.tokenize = lineDiff.tokenize; + jsonDiff.castInput = function (value, options) { + var undefinedReplacement = options.undefinedReplacement, + _options$stringifyRep = options.stringifyReplacer, + stringifyReplacer = _options$stringifyRep === void 0 ? function (k, v) { + return typeof v === 'undefined' ? undefinedReplacement : v; + } : _options$stringifyRep; + return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, ' '); + }; + jsonDiff.equals = function (left, right, options) { + return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'), options); + }; + function diffJson(oldObj, newObj, options) { + return jsonDiff.diff(oldObj, newObj, options); + } + + // This function handles the presence of circular references by bailing out when encountering an + // object that is already on the "stack" of items being processed. Accepts an optional replacer + function canonicalize(obj, stack, replacementStack, replacer, key) { + stack = stack || []; + replacementStack = replacementStack || []; + if (replacer) { + obj = replacer(key, obj); + } + var i; + for (i = 0; i < stack.length; i += 1) { + if (stack[i] === obj) { + return replacementStack[i]; + } + } + var canonicalizedObj; + if ('[object Array]' === Object.prototype.toString.call(obj)) { + stack.push(obj); + canonicalizedObj = new Array(obj.length); + replacementStack.push(canonicalizedObj); + for (i = 0; i < obj.length; i += 1) { + canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key); + } + stack.pop(); + replacementStack.pop(); + return canonicalizedObj; + } + if (obj && obj.toJSON) { + obj = obj.toJSON(); + } + if (_typeof(obj) === 'object' && obj !== null) { + stack.push(obj); + canonicalizedObj = {}; + replacementStack.push(canonicalizedObj); + var sortedKeys = [], + _key; + for (_key in obj) { + /* istanbul ignore else */ + if (Object.prototype.hasOwnProperty.call(obj, _key)) { + sortedKeys.push(_key); + } + } + sortedKeys.sort(); + for (i = 0; i < sortedKeys.length; i += 1) { + _key = sortedKeys[i]; + canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key); + } + stack.pop(); + replacementStack.pop(); + } else { + canonicalizedObj = obj; + } + return canonicalizedObj; + } + + var arrayDiff = new Diff(); + arrayDiff.tokenize = function (value) { + return value.slice(); + }; + arrayDiff.join = arrayDiff.removeEmpty = function (value) { + return value; + }; + function diffArrays(oldArr, newArr, callback) { + return arrayDiff.diff(oldArr, newArr, callback); + } + + function unixToWin(patch) { + if (Array.isArray(patch)) { + return patch.map(unixToWin); + } + return _objectSpread2(_objectSpread2({}, patch), {}, { + hunks: patch.hunks.map(function (hunk) { + return _objectSpread2(_objectSpread2({}, hunk), {}, { + lines: hunk.lines.map(function (line, i) { + var _hunk$lines; + return line.startsWith('\\') || line.endsWith('\r') || (_hunk$lines = hunk.lines[i + 1]) !== null && _hunk$lines !== void 0 && _hunk$lines.startsWith('\\') ? line : line + '\r'; + }) + }); + }) + }); + } + function winToUnix(patch) { + if (Array.isArray(patch)) { + return patch.map(winToUnix); + } + return _objectSpread2(_objectSpread2({}, patch), {}, { + hunks: patch.hunks.map(function (hunk) { + return _objectSpread2(_objectSpread2({}, hunk), {}, { + lines: hunk.lines.map(function (line) { + return line.endsWith('\r') ? line.substring(0, line.length - 1) : line; + }) + }); + }) + }); + } + + /** + * Returns true if the patch consistently uses Unix line endings (or only involves one line and has + * no line endings). + */ + function isUnix(patch) { + if (!Array.isArray(patch)) { + patch = [patch]; + } + return !patch.some(function (index) { + return index.hunks.some(function (hunk) { + return hunk.lines.some(function (line) { + return !line.startsWith('\\') && line.endsWith('\r'); + }); + }); + }); + } + + /** + * Returns true if the patch uses Windows line endings and only Windows line endings. + */ + function isWin(patch) { + if (!Array.isArray(patch)) { + patch = [patch]; + } + return patch.some(function (index) { + return index.hunks.some(function (hunk) { + return hunk.lines.some(function (line) { + return line.endsWith('\r'); + }); + }); + }) && patch.every(function (index) { + return index.hunks.every(function (hunk) { + return hunk.lines.every(function (line, i) { + var _hunk$lines2; + return line.startsWith('\\') || line.endsWith('\r') || ((_hunk$lines2 = hunk.lines[i + 1]) === null || _hunk$lines2 === void 0 ? void 0 : _hunk$lines2.startsWith('\\')); + }); + }); + }); + } + + function parsePatch(uniDiff) { + var diffstr = uniDiff.split(/\n/), + list = [], + i = 0; + function parseIndex() { + var index = {}; + list.push(index); + + // Parse diff metadata + while (i < diffstr.length) { + var line = diffstr[i]; + + // File header found, end parsing diff metadata + if (/^(\-\-\-|\+\+\+|@@)\s/.test(line)) { + break; + } + + // Diff index + var header = /^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/.exec(line); + if (header) { + index.index = header[1]; + } + i++; + } + + // Parse file headers if they are defined. Unified diff requires them, but + // there's no technical issues to have an isolated hunk without file header + parseFileHeader(index); + parseFileHeader(index); + + // Parse hunks + index.hunks = []; + while (i < diffstr.length) { + var _line = diffstr[i]; + if (/^(Index:\s|diff\s|\-\-\-\s|\+\+\+\s|===================================================================)/.test(_line)) { + break; + } else if (/^@@/.test(_line)) { + index.hunks.push(parseHunk()); + } else if (_line) { + throw new Error('Unknown line ' + (i + 1) + ' ' + JSON.stringify(_line)); + } else { + i++; + } + } + } + + // Parses the --- and +++ headers, if none are found, no lines + // are consumed. + function parseFileHeader(index) { + var fileHeader = /^(---|\+\+\+)\s+(.*)\r?$/.exec(diffstr[i]); + if (fileHeader) { + var keyPrefix = fileHeader[1] === '---' ? 'old' : 'new'; + var data = fileHeader[2].split('\t', 2); + var fileName = data[0].replace(/\\\\/g, '\\'); + if (/^".*"$/.test(fileName)) { + fileName = fileName.substr(1, fileName.length - 2); + } + index[keyPrefix + 'FileName'] = fileName; + index[keyPrefix + 'Header'] = (data[1] || '').trim(); + i++; + } + } + + // Parses a hunk + // This assumes that we are at the start of a hunk. + function parseHunk() { + var chunkHeaderIndex = i, + chunkHeaderLine = diffstr[i++], + chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/); + var hunk = { + oldStart: +chunkHeader[1], + oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2], + newStart: +chunkHeader[3], + newLines: typeof chunkHeader[4] === 'undefined' ? 1 : +chunkHeader[4], + lines: [] + }; + + // Unified Diff Format quirk: If the chunk size is 0, + // the first number is one lower than one would expect. + // https://www.artima.com/weblogs/viewpost.jsp?thread=164293 + if (hunk.oldLines === 0) { + hunk.oldStart += 1; + } + if (hunk.newLines === 0) { + hunk.newStart += 1; + } + var addCount = 0, + removeCount = 0; + for (; i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || (_diffstr$i = diffstr[i]) !== null && _diffstr$i !== void 0 && _diffstr$i.startsWith('\\')); i++) { + var _diffstr$i; + var operation = diffstr[i].length == 0 && i != diffstr.length - 1 ? ' ' : diffstr[i][0]; + if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') { + hunk.lines.push(diffstr[i]); + if (operation === '+') { + addCount++; + } else if (operation === '-') { + removeCount++; + } else if (operation === ' ') { + addCount++; + removeCount++; + } + } else { + throw new Error("Hunk at line ".concat(chunkHeaderIndex + 1, " contained invalid line ").concat(diffstr[i])); + } + } + + // Handle the empty block count case + if (!addCount && hunk.newLines === 1) { + hunk.newLines = 0; + } + if (!removeCount && hunk.oldLines === 1) { + hunk.oldLines = 0; + } + + // Perform sanity checking + if (addCount !== hunk.newLines) { + throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1)); + } + if (removeCount !== hunk.oldLines) { + throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1)); + } + return hunk; + } + while (i < diffstr.length) { + parseIndex(); + } + return list; + } + + // Iterator that traverses in the range of [min, max], stepping + // by distance from a given start position. I.e. for [0, 4], with + // start of 2, this will iterate 2, 3, 1, 4, 0. + function distanceIterator (start, minLine, maxLine) { + var wantForward = true, + backwardExhausted = false, + forwardExhausted = false, + localOffset = 1; + return function iterator() { + if (wantForward && !forwardExhausted) { + if (backwardExhausted) { + localOffset++; + } else { + wantForward = false; + } + + // Check if trying to fit beyond text length, and if not, check it fits + // after offset location (or desired location on first iteration) + if (start + localOffset <= maxLine) { + return start + localOffset; + } + forwardExhausted = true; + } + if (!backwardExhausted) { + if (!forwardExhausted) { + wantForward = true; + } + + // Check if trying to fit before text beginning, and if not, check it fits + // before offset location + if (minLine <= start - localOffset) { + return start - localOffset++; + } + backwardExhausted = true; + return iterator(); + } + + // We tried to fit hunk before text beginning and beyond text length, then + // hunk can't fit on the text. Return undefined + }; + } + + function applyPatch(source, uniDiff) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + if (typeof uniDiff === 'string') { + uniDiff = parsePatch(uniDiff); + } + if (Array.isArray(uniDiff)) { + if (uniDiff.length > 1) { + throw new Error('applyPatch only works with a single input.'); + } + uniDiff = uniDiff[0]; + } + if (options.autoConvertLineEndings || options.autoConvertLineEndings == null) { + if (hasOnlyWinLineEndings(source) && isUnix(uniDiff)) { + uniDiff = unixToWin(uniDiff); + } else if (hasOnlyUnixLineEndings(source) && isWin(uniDiff)) { + uniDiff = winToUnix(uniDiff); + } + } + + // Apply the diff to the input + var lines = source.split('\n'), + hunks = uniDiff.hunks, + compareLine = options.compareLine || function (lineNumber, line, operation, patchContent) { + return line === patchContent; + }, + fuzzFactor = options.fuzzFactor || 0, + minLine = 0; + if (fuzzFactor < 0 || !Number.isInteger(fuzzFactor)) { + throw new Error('fuzzFactor must be a non-negative integer'); + } + + // Special case for empty patch. + if (!hunks.length) { + return source; + } + + // Before anything else, handle EOFNL insertion/removal. If the patch tells us to make a change + // to the EOFNL that is redundant/impossible - i.e. to remove a newline that's not there, or add a + // newline that already exists - then we either return false and fail to apply the patch (if + // fuzzFactor is 0) or simply ignore the problem and do nothing (if fuzzFactor is >0). + // If we do need to remove/add a newline at EOF, this will always be in the final hunk: + var prevLine = '', + removeEOFNL = false, + addEOFNL = false; + for (var i = 0; i < hunks[hunks.length - 1].lines.length; i++) { + var line = hunks[hunks.length - 1].lines[i]; + if (line[0] == '\\') { + if (prevLine[0] == '+') { + removeEOFNL = true; + } else if (prevLine[0] == '-') { + addEOFNL = true; + } + } + prevLine = line; + } + if (removeEOFNL) { + if (addEOFNL) { + // This means the final line gets changed but doesn't have a trailing newline in either the + // original or patched version. In that case, we do nothing if fuzzFactor > 0, and if + // fuzzFactor is 0, we simply validate that the source file has no trailing newline. + if (!fuzzFactor && lines[lines.length - 1] == '') { + return false; + } + } else if (lines[lines.length - 1] == '') { + lines.pop(); + } else if (!fuzzFactor) { + return false; + } + } else if (addEOFNL) { + if (lines[lines.length - 1] != '') { + lines.push(''); + } else if (!fuzzFactor) { + return false; + } + } + + /** + * Checks if the hunk can be made to fit at the provided location with at most `maxErrors` + * insertions, substitutions, or deletions, while ensuring also that: + * - lines deleted in the hunk match exactly, and + * - wherever an insertion operation or block of insertion operations appears in the hunk, the + * immediately preceding and following lines of context match exactly + * + * `toPos` should be set such that lines[toPos] is meant to match hunkLines[0]. + * + * If the hunk can be applied, returns an object with properties `oldLineLastI` and + * `replacementLines`. Otherwise, returns null. + */ + function applyHunk(hunkLines, toPos, maxErrors) { + var hunkLinesI = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; + var lastContextLineMatched = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; + var patchedLines = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : []; + var patchedLinesLength = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : 0; + var nConsecutiveOldContextLines = 0; + var nextContextLineMustMatch = false; + for (; hunkLinesI < hunkLines.length; hunkLinesI++) { + var hunkLine = hunkLines[hunkLinesI], + operation = hunkLine.length > 0 ? hunkLine[0] : ' ', + content = hunkLine.length > 0 ? hunkLine.substr(1) : hunkLine; + if (operation === '-') { + if (compareLine(toPos + 1, lines[toPos], operation, content)) { + toPos++; + nConsecutiveOldContextLines = 0; + } else { + if (!maxErrors || lines[toPos] == null) { + return null; + } + patchedLines[patchedLinesLength] = lines[toPos]; + return applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI, false, patchedLines, patchedLinesLength + 1); + } + } + if (operation === '+') { + if (!lastContextLineMatched) { + return null; + } + patchedLines[patchedLinesLength] = content; + patchedLinesLength++; + nConsecutiveOldContextLines = 0; + nextContextLineMustMatch = true; + } + if (operation === ' ') { + nConsecutiveOldContextLines++; + patchedLines[patchedLinesLength] = lines[toPos]; + if (compareLine(toPos + 1, lines[toPos], operation, content)) { + patchedLinesLength++; + lastContextLineMatched = true; + nextContextLineMustMatch = false; + toPos++; + } else { + if (nextContextLineMustMatch || !maxErrors) { + return null; + } + + // Consider 3 possibilities in sequence: + // 1. lines contains a *substitution* not included in the patch context, or + // 2. lines contains an *insertion* not included in the patch context, or + // 3. lines contains a *deletion* not included in the patch context + // The first two options are of course only possible if the line from lines is non-null - + // i.e. only option 3 is possible if we've overrun the end of the old file. + return lines[toPos] && (applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI + 1, false, patchedLines, patchedLinesLength + 1) || applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI, false, patchedLines, patchedLinesLength + 1)) || applyHunk(hunkLines, toPos, maxErrors - 1, hunkLinesI + 1, false, patchedLines, patchedLinesLength); + } + } + } + + // Before returning, trim any unmodified context lines off the end of patchedLines and reduce + // toPos (and thus oldLineLastI) accordingly. This allows later hunks to be applied to a region + // that starts in this hunk's trailing context. + patchedLinesLength -= nConsecutiveOldContextLines; + toPos -= nConsecutiveOldContextLines; + patchedLines.length = patchedLinesLength; + return { + patchedLines: patchedLines, + oldLineLastI: toPos - 1 + }; + } + var resultLines = []; + + // Search best fit offsets for each hunk based on the previous ones + var prevHunkOffset = 0; + for (var _i = 0; _i < hunks.length; _i++) { + var hunk = hunks[_i]; + var hunkResult = void 0; + var maxLine = lines.length - hunk.oldLines + fuzzFactor; + var toPos = void 0; + for (var maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) { + toPos = hunk.oldStart + prevHunkOffset - 1; + var iterator = distanceIterator(toPos, minLine, maxLine); + for (; toPos !== undefined; toPos = iterator()) { + hunkResult = applyHunk(hunk.lines, toPos, maxErrors); + if (hunkResult) { + break; + } + } + if (hunkResult) { + break; + } + } + if (!hunkResult) { + return false; + } + + // Copy everything from the end of where we applied the last hunk to the start of this hunk + for (var _i2 = minLine; _i2 < toPos; _i2++) { + resultLines.push(lines[_i2]); + } + + // Add the lines produced by applying the hunk: + for (var _i3 = 0; _i3 < hunkResult.patchedLines.length; _i3++) { + var _line = hunkResult.patchedLines[_i3]; + resultLines.push(_line); + } + + // Set lower text limit to end of the current hunk, so next ones don't try + // to fit over already patched text + minLine = hunkResult.oldLineLastI + 1; + + // Note the offset between where the patch said the hunk should've applied and where we + // applied it, so we can adjust future hunks accordingly: + prevHunkOffset = toPos + 1 - hunk.oldStart; + } + + // Copy over the rest of the lines from the old text + for (var _i4 = minLine; _i4 < lines.length; _i4++) { + resultLines.push(lines[_i4]); + } + return resultLines.join('\n'); + } + + // Wrapper that supports multiple file patches via callbacks. + function applyPatches(uniDiff, options) { + if (typeof uniDiff === 'string') { + uniDiff = parsePatch(uniDiff); + } + var currentIndex = 0; + function processIndex() { + var index = uniDiff[currentIndex++]; + if (!index) { + return options.complete(); + } + options.loadFile(index, function (err, data) { + if (err) { + return options.complete(err); + } + var updatedContent = applyPatch(data, index, options); + options.patched(index, updatedContent, function (err) { + if (err) { + return options.complete(err); + } + processIndex(); + }); + }); + } + processIndex(); + } + + function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { + if (!options) { + options = {}; + } + if (typeof options === 'function') { + options = { + callback: options + }; + } + if (typeof options.context === 'undefined') { + options.context = 4; + } + if (options.newlineIsToken) { + throw new Error('newlineIsToken may not be used with patch-generation functions, only with diffing functions'); + } + if (!options.callback) { + return diffLinesResultToPatch(diffLines(oldStr, newStr, options)); + } else { + var _options = options, + _callback = _options.callback; + diffLines(oldStr, newStr, _objectSpread2(_objectSpread2({}, options), {}, { + callback: function callback(diff) { + var patch = diffLinesResultToPatch(diff); + _callback(patch); + } + })); + } + function diffLinesResultToPatch(diff) { + // STEP 1: Build up the patch with no "\ No newline at end of file" lines and with the arrays + // of lines containing trailing newline characters. We'll tidy up later... + + if (!diff) { + return; + } + diff.push({ + value: '', + lines: [] + }); // Append an empty value to make cleanup easier + + function contextLines(lines) { + return lines.map(function (entry) { + return ' ' + entry; + }); + } + var hunks = []; + var oldRangeStart = 0, + newRangeStart = 0, + curRange = [], + oldLine = 1, + newLine = 1; + var _loop = function _loop() { + var current = diff[i], + lines = current.lines || splitLines(current.value); + current.lines = lines; + if (current.added || current.removed) { + var _curRange; + // If we have previous context, start with that + if (!oldRangeStart) { + var prev = diff[i - 1]; + oldRangeStart = oldLine; + newRangeStart = newLine; + if (prev) { + curRange = options.context > 0 ? contextLines(prev.lines.slice(-options.context)) : []; + oldRangeStart -= curRange.length; + newRangeStart -= curRange.length; + } + } + + // Output our changes + (_curRange = curRange).push.apply(_curRange, _toConsumableArray(lines.map(function (entry) { + return (current.added ? '+' : '-') + entry; + }))); + + // Track the updated file position + if (current.added) { + newLine += lines.length; + } else { + oldLine += lines.length; + } + } else { + // Identical context lines. Track line changes + if (oldRangeStart) { + // Close out any changes that have been output (or join overlapping) + if (lines.length <= options.context * 2 && i < diff.length - 2) { + var _curRange2; + // Overlapping + (_curRange2 = curRange).push.apply(_curRange2, _toConsumableArray(contextLines(lines))); + } else { + var _curRange3; + // end the range and output + var contextSize = Math.min(lines.length, options.context); + (_curRange3 = curRange).push.apply(_curRange3, _toConsumableArray(contextLines(lines.slice(0, contextSize)))); + var _hunk = { + oldStart: oldRangeStart, + oldLines: oldLine - oldRangeStart + contextSize, + newStart: newRangeStart, + newLines: newLine - newRangeStart + contextSize, + lines: curRange + }; + hunks.push(_hunk); + oldRangeStart = 0; + newRangeStart = 0; + curRange = []; + } + } + oldLine += lines.length; + newLine += lines.length; + } + }; + for (var i = 0; i < diff.length; i++) { + _loop(); + } + + // Step 2: eliminate the trailing `\n` from each line of each hunk, and, where needed, add + // "\ No newline at end of file". + for (var _i = 0, _hunks = hunks; _i < _hunks.length; _i++) { + var hunk = _hunks[_i]; + for (var _i2 = 0; _i2 < hunk.lines.length; _i2++) { + if (hunk.lines[_i2].endsWith('\n')) { + hunk.lines[_i2] = hunk.lines[_i2].slice(0, -1); + } else { + hunk.lines.splice(_i2 + 1, 0, '\\ No newline at end of file'); + _i2++; // Skip the line we just added, then continue iterating + } + } + } + return { + oldFileName: oldFileName, + newFileName: newFileName, + oldHeader: oldHeader, + newHeader: newHeader, + hunks: hunks + }; + } + } + function formatPatch(diff) { + if (Array.isArray(diff)) { + return diff.map(formatPatch).join('\n'); + } + var ret = []; + if (diff.oldFileName == diff.newFileName) { + ret.push('Index: ' + diff.oldFileName); + } + ret.push('==================================================================='); + ret.push('--- ' + diff.oldFileName + (typeof diff.oldHeader === 'undefined' ? '' : '\t' + diff.oldHeader)); + ret.push('+++ ' + diff.newFileName + (typeof diff.newHeader === 'undefined' ? '' : '\t' + diff.newHeader)); + for (var i = 0; i < diff.hunks.length; i++) { + var hunk = diff.hunks[i]; + // Unified Diff Format quirk: If the chunk size is 0, + // the first number is one lower than one would expect. + // https://www.artima.com/weblogs/viewpost.jsp?thread=164293 + if (hunk.oldLines === 0) { + hunk.oldStart -= 1; + } + if (hunk.newLines === 0) { + hunk.newStart -= 1; + } + ret.push('@@ -' + hunk.oldStart + ',' + hunk.oldLines + ' +' + hunk.newStart + ',' + hunk.newLines + ' @@'); + ret.push.apply(ret, hunk.lines); + } + return ret.join('\n') + '\n'; + } + function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { + var _options2; + if (typeof options === 'function') { + options = { + callback: options + }; + } + if (!((_options2 = options) !== null && _options2 !== void 0 && _options2.callback)) { + var patchObj = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options); + if (!patchObj) { + return; + } + return formatPatch(patchObj); + } else { + var _options3 = options, + _callback2 = _options3.callback; + structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, _objectSpread2(_objectSpread2({}, options), {}, { + callback: function callback(patchObj) { + if (!patchObj) { + _callback2(); + } else { + _callback2(formatPatch(patchObj)); + } + } + })); + } + } + function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) { + return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options); + } + + /** + * Split `text` into an array of lines, including the trailing newline character (where present) + */ + function splitLines(text) { + var hasTrailingNl = text.endsWith('\n'); + var result = text.split('\n').map(function (line) { + return line + '\n'; + }); + if (hasTrailingNl) { + result.pop(); + } else { + result.push(result.pop().slice(0, -1)); + } + return result; + } + + function arrayEqual(a, b) { + if (a.length !== b.length) { + return false; + } + return arrayStartsWith(a, b); + } + function arrayStartsWith(array, start) { + if (start.length > array.length) { + return false; + } + for (var i = 0; i < start.length; i++) { + if (start[i] !== array[i]) { + return false; + } + } + return true; + } + + function calcLineCount(hunk) { + var _calcOldNewLineCount = calcOldNewLineCount(hunk.lines), + oldLines = _calcOldNewLineCount.oldLines, + newLines = _calcOldNewLineCount.newLines; + if (oldLines !== undefined) { + hunk.oldLines = oldLines; + } else { + delete hunk.oldLines; + } + if (newLines !== undefined) { + hunk.newLines = newLines; + } else { + delete hunk.newLines; + } + } + function merge(mine, theirs, base) { + mine = loadPatch(mine, base); + theirs = loadPatch(theirs, base); + var ret = {}; + + // For index we just let it pass through as it doesn't have any necessary meaning. + // Leaving sanity checks on this to the API consumer that may know more about the + // meaning in their own context. + if (mine.index || theirs.index) { + ret.index = mine.index || theirs.index; + } + if (mine.newFileName || theirs.newFileName) { + if (!fileNameChanged(mine)) { + // No header or no change in ours, use theirs (and ours if theirs does not exist) + ret.oldFileName = theirs.oldFileName || mine.oldFileName; + ret.newFileName = theirs.newFileName || mine.newFileName; + ret.oldHeader = theirs.oldHeader || mine.oldHeader; + ret.newHeader = theirs.newHeader || mine.newHeader; + } else if (!fileNameChanged(theirs)) { + // No header or no change in theirs, use ours + ret.oldFileName = mine.oldFileName; + ret.newFileName = mine.newFileName; + ret.oldHeader = mine.oldHeader; + ret.newHeader = mine.newHeader; + } else { + // Both changed... figure it out + ret.oldFileName = selectField(ret, mine.oldFileName, theirs.oldFileName); + ret.newFileName = selectField(ret, mine.newFileName, theirs.newFileName); + ret.oldHeader = selectField(ret, mine.oldHeader, theirs.oldHeader); + ret.newHeader = selectField(ret, mine.newHeader, theirs.newHeader); + } + } + ret.hunks = []; + var mineIndex = 0, + theirsIndex = 0, + mineOffset = 0, + theirsOffset = 0; + while (mineIndex < mine.hunks.length || theirsIndex < theirs.hunks.length) { + var mineCurrent = mine.hunks[mineIndex] || { + oldStart: Infinity + }, + theirsCurrent = theirs.hunks[theirsIndex] || { + oldStart: Infinity + }; + if (hunkBefore(mineCurrent, theirsCurrent)) { + // This patch does not overlap with any of the others, yay. + ret.hunks.push(cloneHunk(mineCurrent, mineOffset)); + mineIndex++; + theirsOffset += mineCurrent.newLines - mineCurrent.oldLines; + } else if (hunkBefore(theirsCurrent, mineCurrent)) { + // This patch does not overlap with any of the others, yay. + ret.hunks.push(cloneHunk(theirsCurrent, theirsOffset)); + theirsIndex++; + mineOffset += theirsCurrent.newLines - theirsCurrent.oldLines; + } else { + // Overlap, merge as best we can + var mergedHunk = { + oldStart: Math.min(mineCurrent.oldStart, theirsCurrent.oldStart), + oldLines: 0, + newStart: Math.min(mineCurrent.newStart + mineOffset, theirsCurrent.oldStart + theirsOffset), + newLines: 0, + lines: [] + }; + mergeLines(mergedHunk, mineCurrent.oldStart, mineCurrent.lines, theirsCurrent.oldStart, theirsCurrent.lines); + theirsIndex++; + mineIndex++; + ret.hunks.push(mergedHunk); + } + } + return ret; + } + function loadPatch(param, base) { + if (typeof param === 'string') { + if (/^@@/m.test(param) || /^Index:/m.test(param)) { + return parsePatch(param)[0]; + } + if (!base) { + throw new Error('Must provide a base reference or pass in a patch'); + } + return structuredPatch(undefined, undefined, base, param); + } + return param; + } + function fileNameChanged(patch) { + return patch.newFileName && patch.newFileName !== patch.oldFileName; + } + function selectField(index, mine, theirs) { + if (mine === theirs) { + return mine; + } else { + index.conflict = true; + return { + mine: mine, + theirs: theirs + }; + } + } + function hunkBefore(test, check) { + return test.oldStart < check.oldStart && test.oldStart + test.oldLines < check.oldStart; + } + function cloneHunk(hunk, offset) { + return { + oldStart: hunk.oldStart, + oldLines: hunk.oldLines, + newStart: hunk.newStart + offset, + newLines: hunk.newLines, + lines: hunk.lines + }; + } + function mergeLines(hunk, mineOffset, mineLines, theirOffset, theirLines) { + // This will generally result in a conflicted hunk, but there are cases where the context + // is the only overlap where we can successfully merge the content here. + var mine = { + offset: mineOffset, + lines: mineLines, + index: 0 + }, + their = { + offset: theirOffset, + lines: theirLines, + index: 0 + }; + + // Handle any leading content + insertLeading(hunk, mine, their); + insertLeading(hunk, their, mine); + + // Now in the overlap content. Scan through and select the best changes from each. + while (mine.index < mine.lines.length && their.index < their.lines.length) { + var mineCurrent = mine.lines[mine.index], + theirCurrent = their.lines[their.index]; + if ((mineCurrent[0] === '-' || mineCurrent[0] === '+') && (theirCurrent[0] === '-' || theirCurrent[0] === '+')) { + // Both modified ... + mutualChange(hunk, mine, their); + } else if (mineCurrent[0] === '+' && theirCurrent[0] === ' ') { + var _hunk$lines; + // Mine inserted + (_hunk$lines = hunk.lines).push.apply(_hunk$lines, _toConsumableArray(collectChange(mine))); + } else if (theirCurrent[0] === '+' && mineCurrent[0] === ' ') { + var _hunk$lines2; + // Theirs inserted + (_hunk$lines2 = hunk.lines).push.apply(_hunk$lines2, _toConsumableArray(collectChange(their))); + } else if (mineCurrent[0] === '-' && theirCurrent[0] === ' ') { + // Mine removed or edited + removal(hunk, mine, their); + } else if (theirCurrent[0] === '-' && mineCurrent[0] === ' ') { + // Their removed or edited + removal(hunk, their, mine, true); + } else if (mineCurrent === theirCurrent) { + // Context identity + hunk.lines.push(mineCurrent); + mine.index++; + their.index++; + } else { + // Context mismatch + conflict(hunk, collectChange(mine), collectChange(their)); + } + } + + // Now push anything that may be remaining + insertTrailing(hunk, mine); + insertTrailing(hunk, their); + calcLineCount(hunk); + } + function mutualChange(hunk, mine, their) { + var myChanges = collectChange(mine), + theirChanges = collectChange(their); + if (allRemoves(myChanges) && allRemoves(theirChanges)) { + // Special case for remove changes that are supersets of one another + if (arrayStartsWith(myChanges, theirChanges) && skipRemoveSuperset(their, myChanges, myChanges.length - theirChanges.length)) { + var _hunk$lines3; + (_hunk$lines3 = hunk.lines).push.apply(_hunk$lines3, _toConsumableArray(myChanges)); + return; + } else if (arrayStartsWith(theirChanges, myChanges) && skipRemoveSuperset(mine, theirChanges, theirChanges.length - myChanges.length)) { + var _hunk$lines4; + (_hunk$lines4 = hunk.lines).push.apply(_hunk$lines4, _toConsumableArray(theirChanges)); + return; + } + } else if (arrayEqual(myChanges, theirChanges)) { + var _hunk$lines5; + (_hunk$lines5 = hunk.lines).push.apply(_hunk$lines5, _toConsumableArray(myChanges)); + return; + } + conflict(hunk, myChanges, theirChanges); + } + function removal(hunk, mine, their, swap) { + var myChanges = collectChange(mine), + theirChanges = collectContext(their, myChanges); + if (theirChanges.merged) { + var _hunk$lines6; + (_hunk$lines6 = hunk.lines).push.apply(_hunk$lines6, _toConsumableArray(theirChanges.merged)); + } else { + conflict(hunk, swap ? theirChanges : myChanges, swap ? myChanges : theirChanges); + } + } + function conflict(hunk, mine, their) { + hunk.conflict = true; + hunk.lines.push({ + conflict: true, + mine: mine, + theirs: their + }); + } + function insertLeading(hunk, insert, their) { + while (insert.offset < their.offset && insert.index < insert.lines.length) { + var line = insert.lines[insert.index++]; + hunk.lines.push(line); + insert.offset++; + } + } + function insertTrailing(hunk, insert) { + while (insert.index < insert.lines.length) { + var line = insert.lines[insert.index++]; + hunk.lines.push(line); + } + } + function collectChange(state) { + var ret = [], + operation = state.lines[state.index][0]; + while (state.index < state.lines.length) { + var line = state.lines[state.index]; + + // Group additions that are immediately after subtractions and treat them as one "atomic" modify change. + if (operation === '-' && line[0] === '+') { + operation = '+'; + } + if (operation === line[0]) { + ret.push(line); + state.index++; + } else { + break; + } + } + return ret; + } + function collectContext(state, matchChanges) { + var changes = [], + merged = [], + matchIndex = 0, + contextChanges = false, + conflicted = false; + while (matchIndex < matchChanges.length && state.index < state.lines.length) { + var change = state.lines[state.index], + match = matchChanges[matchIndex]; + + // Once we've hit our add, then we are done + if (match[0] === '+') { + break; + } + contextChanges = contextChanges || change[0] !== ' '; + merged.push(match); + matchIndex++; + + // Consume any additions in the other block as a conflict to attempt + // to pull in the remaining context after this + if (change[0] === '+') { + conflicted = true; + while (change[0] === '+') { + changes.push(change); + change = state.lines[++state.index]; + } + } + if (match.substr(1) === change.substr(1)) { + changes.push(change); + state.index++; + } else { + conflicted = true; + } + } + if ((matchChanges[matchIndex] || '')[0] === '+' && contextChanges) { + conflicted = true; + } + if (conflicted) { + return changes; + } + while (matchIndex < matchChanges.length) { + merged.push(matchChanges[matchIndex++]); + } + return { + merged: merged, + changes: changes + }; + } + function allRemoves(changes) { + return changes.reduce(function (prev, change) { + return prev && change[0] === '-'; + }, true); + } + function skipRemoveSuperset(state, removeChanges, delta) { + for (var i = 0; i < delta; i++) { + var changeContent = removeChanges[removeChanges.length - delta + i].substr(1); + if (state.lines[state.index + i] !== ' ' + changeContent) { + return false; + } + } + state.index += delta; + return true; + } + function calcOldNewLineCount(lines) { + var oldLines = 0; + var newLines = 0; + lines.forEach(function (line) { + if (typeof line !== 'string') { + var myCount = calcOldNewLineCount(line.mine); + var theirCount = calcOldNewLineCount(line.theirs); + if (oldLines !== undefined) { + if (myCount.oldLines === theirCount.oldLines) { + oldLines += myCount.oldLines; + } else { + oldLines = undefined; + } + } + if (newLines !== undefined) { + if (myCount.newLines === theirCount.newLines) { + newLines += myCount.newLines; + } else { + newLines = undefined; + } + } + } else { + if (newLines !== undefined && (line[0] === '+' || line[0] === ' ')) { + newLines++; + } + if (oldLines !== undefined && (line[0] === '-' || line[0] === ' ')) { + oldLines++; + } + } + }); + return { + oldLines: oldLines, + newLines: newLines + }; + } + + function reversePatch(structuredPatch) { + if (Array.isArray(structuredPatch)) { + return structuredPatch.map(reversePatch).reverse(); + } + return _objectSpread2(_objectSpread2({}, structuredPatch), {}, { + oldFileName: structuredPatch.newFileName, + oldHeader: structuredPatch.newHeader, + newFileName: structuredPatch.oldFileName, + newHeader: structuredPatch.oldHeader, + hunks: structuredPatch.hunks.map(function (hunk) { + return { + oldLines: hunk.newLines, + oldStart: hunk.newStart, + newLines: hunk.oldLines, + newStart: hunk.oldStart, + lines: hunk.lines.map(function (l) { + if (l.startsWith('-')) { + return "+".concat(l.slice(1)); + } + if (l.startsWith('+')) { + return "-".concat(l.slice(1)); + } + return l; + }) + }; + }) + }); + } + + // See: http://code.google.com/p/google-diff-match-patch/wiki/API + function convertChangesToDMP(changes) { + var ret = [], + change, + operation; + for (var i = 0; i < changes.length; i++) { + change = changes[i]; + if (change.added) { + operation = 1; + } else if (change.removed) { + operation = -1; + } else { + operation = 0; + } + ret.push([operation, change.value]); + } + return ret; + } + + function convertChangesToXML(changes) { + var ret = []; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + if (change.added) { + ret.push(''); + } else if (change.removed) { + ret.push(''); + } + ret.push(escapeHTML(change.value)); + if (change.added) { + ret.push(''); + } else if (change.removed) { + ret.push(''); + } + } + return ret.join(''); + } + function escapeHTML(s) { + var n = s; + n = n.replace(/&/g, '&'); + n = n.replace(//g, '>'); + n = n.replace(/"/g, '"'); + return n; + } + + exports.Diff = Diff; + exports.applyPatch = applyPatch; + exports.applyPatches = applyPatches; + exports.canonicalize = canonicalize; + exports.convertChangesToDMP = convertChangesToDMP; + exports.convertChangesToXML = convertChangesToXML; + exports.createPatch = createPatch; + exports.createTwoFilesPatch = createTwoFilesPatch; + exports.diffArrays = diffArrays; + exports.diffChars = diffChars; + exports.diffCss = diffCss; + exports.diffJson = diffJson; + exports.diffLines = diffLines; + exports.diffSentences = diffSentences; + exports.diffTrimmedLines = diffTrimmedLines; + exports.diffWords = diffWords; + exports.diffWordsWithSpace = diffWordsWithSpace; + exports.formatPatch = formatPatch; + exports.merge = merge; + exports.parsePatch = parsePatch; + exports.reversePatch = reversePatch; + exports.structuredPatch = structuredPatch; + +})); From 928946dfdf8156f01d9f7daf626d569b4b89e162 Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 21 Mar 2025 00:22:00 +0100 Subject: [PATCH 53/92] Add diff viewer functionality for APIs integrations. see #109 --- api_webchat/index.html | 1 + api_webchat/messagesArea.js | 78 +++++++++++++++++++++++++++---------- js/mzta-menus.js | 4 +- mzta-background.js | 10 ++--- 4 files changed, 66 insertions(+), 27 deletions(-) diff --git a/api_webchat/index.html b/api_webchat/index.html index 7989868d..5d803548 100644 --- a/api_webchat/index.html +++ b/api_webchat/index.html @@ -17,6 +17,7 @@ + diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index 9b1b2d98..c4888060 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -90,6 +90,17 @@ messagesAreaStyle.textContent = ` color: navy; margin-bottom: var(--margin); } + /* diff viewer */ + .added { + background-color: #d4fcdc; + display: inline; + } + + .removed { + background-color: #fddddd; + display: inline; + text-decoration: line-through; + } `; messagesAreaTemplate.content.appendChild(messagesAreaStyle); @@ -207,27 +218,6 @@ class MessagesArea extends HTMLElement { } addActionButtons(promptData = null) { - // ============================== TESTING - // promptData = { - // action: "1", - // tabId: 1, - // mailMessageId: 1 - // } - // let browser = { - // i18n: { - // getMessage: async function(key) { - // return key; - // } - // }, - // storage: { - // sync: { - // get: async function(key) { - // return 'apitest'; - // } - // } - // } - // } - // ============================== TESTING - END if(promptData == null) { return; } const actionButtons = document.createElement('div'); actionButtons.classList.add('action-buttons'); @@ -257,6 +247,18 @@ class MessagesArea extends HTMLElement { browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); // close window }); if(promptData.action != 0) { actionButtons.appendChild(actionButton); } + + // diff viewer button + if(promptData.prompt_info?.use_diff_viewer == "1") { + const diffvButton = document.createElement('button'); + diffvButton.textContent = 'Show differences'; + diffvButton.addEventListener('click', async () => { + let strippedText = fullTextHTMLAtAssignment.replace(/<\/?[^>]+(>|$)/g, ""); + this.appendDiffViewer(promptData.prompt_info?.selection_text, strippedText); + }); + actionButtons.appendChild(diffvButton); + } + actionButtons.appendChild(closeButton); this.messages.appendChild(actionButtons); this.scrollToBottom(); @@ -268,6 +270,40 @@ class MessagesArea extends HTMLElement { this.scrollToBottom(); } + appendDiffViewer(originalText, newText) { + const wordDiff = Diff.diffWords(originalText, newText); + + const messageElement = document.createElement('div'); + messageElement.classList.add('message', 'bot'); + + // Iterate over each part of the diff to create the HTML output + wordDiff.forEach(part => { + const diffElement = document.createElement("span"); + + // Apply a different class depending on whether the word is added, removed, or unchanged + if (part.added) { + diffElement.className = "added"; + diffElement.textContent = part.value; + } else if (part.removed) { + diffElement.className = "removed"; + diffElement.textContent = part.value; + } else { + diffElement.textContent = part.value; + } + + // Add the element to the container + messageElement.appendChild(diffElement); + }); + + const header = document.createElement('h2'); + header.textContent = "Diff viewer"; + this.messages.appendChild(header); + + this.messages.appendChild(messageElement); + this.addDivider(); + this.scrollToBottom(); + } + flushAccumulatingMessage() { if (this.accumulatingMessageEl) { // Collect all tokens in a full text diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 36f236cd..03b53868 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -106,7 +106,9 @@ export class mzta_Menus { let selection_text = ''; let only_typed_text = ''; selection_text = msg_text.selection.replace(/\s+/g, ' ').trim(); + curr_prompt.selection_text = selection_text; body_text = msg_text.text.replace(/\s+/g, ' ').trim(); + curr_prompt.body_text = body_text; only_typed_text = msg_text.only_typed_text.replace(/\s+/g, ' ').trim(); //open chatgpt window //console.log("Click menu item..."); @@ -260,7 +262,7 @@ export class mzta_Menus { break; } }else{ // Classic prompts for the API webchat - this.openChatGPT(fullPrompt, curr_prompt.action, tabs[0].id, curr_prompt.name, curr_prompt.need_custom_text); + this.openChatGPT(fullPrompt, curr_prompt.action, tabs[0].id, curr_prompt.name, curr_prompt.need_custom_text, curr_prompt); taWorkingStatus.stopWorking(); return {ok:'1'}; } diff --git a/mzta-background.js b/mzta-background.js index e51417fe..835b3de7 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -296,7 +296,7 @@ browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => } }); -async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_custom_text = 0) { +async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_custom_text = 0, prompt_info = {}) { let prefs = await browser.storage.sync.get(prefs_default); taLog.changeDebug(prefs.do_debug); prefs = checkScreenDimensions(prefs); @@ -405,7 +405,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ return; } //console.log(">>>>>>>>>> sender: " + JSON.stringify(sender)); - browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId2, do_custom_text: do_custom_text}); + browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId2, do_custom_text: do_custom_text, prompt_info: prompt_info}); taLog.log('[OpenAI ChatGPT] Connection succeded!'); browser.runtime.onMessage.removeListener(listener2); } @@ -455,7 +455,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ return; } //console.log(">>>>>>>>>> sender: " + JSON.stringify(sender)); - browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId5, do_custom_text: do_custom_text}); + browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId5, do_custom_text: do_custom_text, prompt_info: prompt_info}); taLog.log('[Google Gemini] Connection succeded!'); browser.runtime.onMessage.removeListener(listener5); } @@ -511,7 +511,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ browser.tabs.sendMessage(createdTab3.id, { command: "api_error", error: browser.i18n.getMessage('ollama_empty_model')}); return; } - browser.tabs.sendMessage(createdTab3.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId3, do_custom_text: do_custom_text}); + browser.tabs.sendMessage(createdTab3.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId3, do_custom_text: do_custom_text, prompt_info: prompt_info}); taLog.log('[Ollama API] Connection succeded!'); browser.runtime.onMessage.removeListener(listener3); } @@ -563,7 +563,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ return; } - browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId4, do_custom_text: do_custom_text}); + browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId4, do_custom_text: do_custom_text, prompt_info: prompt_info}); taLog.log('[OpenAI Comp API] Connection succeded!'); browser.runtime.onMessage.removeListener(listener4); } From ba4024ddbf2ee1b04f786341cefb6a4e481331c8 Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 21 Mar 2025 18:34:30 +0100 Subject: [PATCH 54/92] chatgtpweb: Include diff library in content scripts for enhanced functionality. see #109 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index fa3ce5cf..a13a85be 100644 --- a/manifest.json +++ b/manifest.json @@ -59,7 +59,7 @@ "content_scripts": [ { "matches": ["https://*.chatgpt.com/*"], - "js": ["js/tb115_segmenter/tb115_loader.js"], + "js": ["js/tb115_segmenter/tb115_loader.js","js/lib/diff.js"], "all_frames": true, "run_at": "document_start" } From 96d6029b221c153161fbc12ffd5e8f2353b7b5b9 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 21 Mar 2025 21:27:54 +0100 Subject: [PATCH 55/92] Improve error handling for calendar event dialog by refining error message processing --- js/mzta-menus.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 36f236cd..b2a93c6c 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -238,7 +238,8 @@ export class mzta_Menus { taWorkingStatus.stopWorking(); return {ok:'1'}; } else { - if (result_openCalendarEventDialog.error.startsWith("|>>")) { + let err = result_openCalendarEventDialog.error; + if (err && typeof err === 'string' && err.startsWith('|>>')) { result_openCalendarEventDialog.error = browser.i18n.getMessage(result_openCalendarEventDialog.error.substring(3)); } browser.tabs.sendMessage(tabs[0].id, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage("calendar_opening_dialog_error") + ": " + result_openCalendarEventDialog.error }); From b7b0e931fdc3ccca92d8a054de880cc24b793675 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 21 Mar 2025 21:29:20 +0100 Subject: [PATCH 56/92] version set to 3.3.0pre1 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index fa3ce5cf..a4b836cc 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "ThunderAI", "description": "__MSG_extensionDescription__", - "version": "3.3.0", + "version": "3.3.0pre1", "author": "Mic (m@micz.it)", "homepage_url": "https://micz.it/thunderbird-addon-thunderai/", "browser_specific_settings": { From 7c606bcb71a3f8d0f3b6c2221da14bd17878cccb Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 22 Mar 2025 17:17:17 +0100 Subject: [PATCH 57/92] Update diff viewer to use body text as fallback for original text. see #109 --- api_webchat/messagesArea.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index c4888060..cae92c4e 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -254,7 +254,11 @@ class MessagesArea extends HTMLElement { diffvButton.textContent = 'Show differences'; diffvButton.addEventListener('click', async () => { let strippedText = fullTextHTMLAtAssignment.replace(/<\/?[^>]+(>|$)/g, ""); - this.appendDiffViewer(promptData.prompt_info?.selection_text, strippedText); + let originalText = promptData.prompt_info?.selection_text; + if((originalText == null) || (originalText == "")) { + originalText = promptData.prompt_info?.body_text; + } + this.appendDiffViewer(originalText, strippedText); }); actionButtons.appendChild(diffvButton); } From bebbfdd74cb6e75f8af838b32ef2c5fd17997301 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 22 Mar 2025 17:42:23 +0100 Subject: [PATCH 58/92] Add dark mode styles for added and removed message highlights. see #109 --- api_webchat/messagesArea.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index cae92c4e..bfc02123 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -45,7 +45,6 @@ messagesAreaStyle.textContent = ` line-height: 1.3; padding: 5px; border-radius: 10px; - } .message p{ margin: 0; @@ -95,12 +94,19 @@ messagesAreaStyle.textContent = ` background-color: #d4fcdc; display: inline; } - .removed { background-color: #fddddd; display: inline; text-decoration: line-through; } + @media (prefers-color-scheme: dark) { + .added { + background-color:rgb(0, 94, 0); + } + .removed { + background-color:rgb(90, 0, 0); + } + } `; messagesAreaTemplate.content.appendChild(messagesAreaStyle); From 23adcbf48fab00197e0c1059159b95eb7325689f Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 22 Mar 2025 17:58:13 +0100 Subject: [PATCH 59/92] Add diff viewer functionality and related UI elements. work in progress. see #109 --- _locales/en/messages.json | 4 +++ js/mzta-chatgpt.js | 70 +++++++++++++++++++++++++++++++++++++-- mzta-background.js | 7 ++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b654ddf6..c380ce14 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1214,5 +1214,9 @@ "noActiveCalendar": { "message": "No active calendar found!", "description": "" + }, + "btn_show_differences": { + "message": "Show differences", + "description": "" } } \ No newline at end of file diff --git a/js/mzta-chatgpt.js b/js/mzta-chatgpt.js index 182ae6f4..d3abe0d8 100644 --- a/js/mzta-chatgpt.js +++ b/js/mzta-chatgpt.js @@ -20,7 +20,7 @@ // Some original methods derived from https://github.com/KudoAI/chatgpt.js/blob/7eb8463cd61143fa9e1d5a8ec3c14d3c1b286e54/chatgpt.js // Using a full string to inject it in the ChatGPT page to avoid any security error -export const mzta_script = ` +//export const mzta_script = ` let force_go = false; let do_force_completion = false; let current_message = null; @@ -99,7 +99,7 @@ function chatpgt_scrollToBottom () { function addCustomDiv(prompt_action,tabId,mailMessageId) { // Create