From 6cc01190796a0dd7730d7edd3f4174b04ac64d2d Mon Sep 17 00:00:00 2001 From: Evernight Date: Mon, 19 Oct 2026 18:10:38 -0700 Subject: [PATCH 1/2] feat: macOS desktop app (Electron) + iOS build fixes and macOS UX polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apps/desktop: Electron shell that runs the QuireNote server in-process (free port + /api/health wait), serves the built web app; esbuild bundles the server, @electron/rebuild compiles better-sqlite3 for the Electron ABI, sqlite-vec ships as vec0.dylib; electron-builder produces unsigned macOS zip - server: QUIRENOTE_SQLITE3_BINDING / QUIREENOTE_SQLITE_VEC_EXT let the packaged app point at raw native binaries (normal path unchanged) - web: show ⌘ instead of Ctrl on macOS (sidebar, settings, manual, i18n strings via modKey); server address hint consistent - web fix: in the native shell with no server configured, API calls now fail fast instead of resolving to the local index.html blob (crashed the home screen via persisted query cache) - iOS: NSAllowsArbitraryLoads so the app can reach http self-hosted servers; SPM Package.resolved committed; docs for local simulator builds - docs: desktop.md (Electron build) + mobile.md local iOS build steps; README platforms badge adds macOS Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .gitignore | 2 + README.md | 2 +- README.zh-CN.md | 2 +- apps/desktop/build/icon.png | Bin 0 -> 42180 bytes apps/desktop/electron-builder.yml | 18 + apps/desktop/native/package-lock.json | 113 + apps/desktop/native/package.json | 9 + apps/desktop/package.json | 21 + apps/desktop/scripts/bundle.mjs | 53 + apps/desktop/scripts/natives.mjs | 45 + apps/desktop/src/main.mjs | 102 + apps/desktop/src/server-entry.mjs | 26 + .../xcshareddata/swiftpm/Package.resolved | 15 + apps/mobile/ios/App/App/Info.plist | 5 + apps/server/src/store/sqlite.ts | 25 +- apps/web/src/components/AppSidebar.tsx | 8 +- apps/web/src/features/editor/embeds.tsx | 3 +- apps/web/src/features/manual/ManualDialog.tsx | 7 +- apps/web/src/i18n/en.ts | 4 +- apps/web/src/i18n/zh.ts | 4 +- apps/web/src/lib/api.ts | 15 + apps/web/src/lib/clipboard.ts | 3 +- apps/web/src/lib/hotkeys.ts | 5 +- apps/web/src/routes/settings.tsx | 4 +- docs/desktop.md | 38 + docs/mobile.md | 14 + pnpm-lock.yaml | 2085 ++++++++++++++++- pnpm-workspace.yaml | 2 + 28 files changed, 2588 insertions(+), 42 deletions(-) create mode 100644 apps/desktop/build/icon.png create mode 100644 apps/desktop/electron-builder.yml create mode 100644 apps/desktop/native/package-lock.json create mode 100644 apps/desktop/native/package.json create mode 100644 apps/desktop/package.json create mode 100644 apps/desktop/scripts/bundle.mjs create mode 100644 apps/desktop/scripts/natives.mjs create mode 100644 apps/desktop/src/main.mjs create mode 100644 apps/desktop/src/server-entry.mjs create mode 100644 apps/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 docs/desktop.md diff --git a/.gitignore b/.gitignore index d5caf6e..ed9cb68 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ apps/mobile/android/app/src/main/assets/public/ apps/mobile/android/capacitor-cordova-android-plugins/ apps/mobile/ios/App/App/public/ apps/mobile/ios/DerivedData/ +apps/desktop/release/ +apps/desktop/native/bin/ diff --git a/README.md b/README.md index f3de66c..9352d90 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Read your sources, ask with citations, write and learn — in one workspace you [![CI](https://img.shields.io/github/actions/workflow/status/Mar7thLover/QuireNote/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/Mar7thLover/QuireNote/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-C06240?style=flat-square)](LICENSE) [![Node](https://img.shields.io/badge/node-%E2%89%A522.12-5A5A55?style=flat-square)](https://nodejs.org) -[![Platforms](https://img.shields.io/badge/platforms-web%20%C2%B7%20iOS%20%C2%B7%20Android-5A5A55?style=flat-square)](docs/mobile.md) +[![Platforms](https://img.shields.io/badge/platforms-web%20%C2%B7%20macOS%20%C2%B7%20iOS%20%C2%B7%20Android-5A5A55?style=flat-square)](docs/mobile.md) [![Docker](https://img.shields.io/badge/deploy-Docker-5A5A55?style=flat-square)](#docker) **English** · [简体中文](README.zh-CN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 2838f21..314b664 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -10,7 +10,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/Mar7thLover/QuireNote/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/Mar7thLover/QuireNote/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-C06240?style=flat-square)](LICENSE) [![Node](https://img.shields.io/badge/node-%E2%89%A522.12-5A5A55?style=flat-square)](https://nodejs.org) -[![Platforms](https://img.shields.io/badge/platforms-web%20%C2%B7%20iOS%20%C2%B7%20Android-5A5A55?style=flat-square)](docs/mobile.md) +[![Platforms](https://img.shields.io/badge/platforms-web%20%C2%B7%20macOS%20%C2%B7%20iOS%20%C2%B7%20Android-5A5A55?style=flat-square)](docs/mobile.md) [![Docker](https://img.shields.io/badge/deploy-Docker-5A5A55?style=flat-square)](#docker-一键部署) [English](README.md) · **简体中文** diff --git a/apps/desktop/build/icon.png b/apps/desktop/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f7e67f3cb688e539dce190685f82dceb8b986b1e GIT binary patch literal 42180 zcmZr&c_5Ts)PL-iETvLpd0VI?MaVWon@XY8ZdA7H+4mup%2tG|qh!fi$Zn93WY03T zvG0swn6b@#_ZijO`+eX1qvl!ez2}^J&hPxrx$|68U6o@S-!=$>I4)m0e*=P;!9SUy zEsWsLJi7EJ1aU)`&#T;Wjh)$G|0u5G9mz(Oa+xb@TI(fedZZT@ryb6)c^~mu;F8h( zef#6La`RPnG4Tu#1BJeF*93lgcFON*^ZBdV!VipCjX(Oo`4klN%G$0=#JK)q4ObM@ zvxR?9v!M98meXymK96Z^+A}NTsZW65MnZz-Zk(?{aVEV{pWMc#~OD&%J@25G7Fy7MR>4k)YlGMTdwFc(C_)Ur2~4I+1NnaT4$__ zh`L%Y8phkRPz1{rf{b=lOGCa9L|1l24@jfltZCrEifgI0IA(J_(4Ij))9pk{#eT;B zPA;1-i~c;MecqnMC-l#!^=atuC-3SqB{Xo&Q*1nzp?>v0k9)O$)q-x>AM(ALFPh8t zEzgrK6ur)cVKGSkWfT85V-HCWA9$8vwK-1Wv{Gs4E{7s^8oPr0M-b(2OVxN*+c_9R z;~644xt_aLb<^j)qE^Ll$mHl^22?J{EGdoV8U$+L{|?W7h9+LP7LF% zIH3XJvg*HX{WK3F+n3h$KuhA7GL`(&(Gpy~xw^85-fJAwZ)*~_T3Hcg zsFnV&=AMWnX@`tNkJbH0pHIY5dQ9BjuMSOZuN~CXAZH1EGiJBf zEE5a9nlB7?p0lYg0@r9|wKAT;ARH$CZH58fP|2fVbwFUPe9)CYpYJCklcb^7|9%Wk z#MMolap!x9ka~taufbD2tT2-}eQ##pANz6s>?xb36}|5Fo}o$8~FlOJ@G^0GXC6jjoQzC-e6LMg9FS;f}IW$zLoG37cKdtTR?XTS^vLynxkI0=B)CnJL%H z*&j0uIdQm;(gsWkO?%&^GhMFP+!cTMwn;vp)F0C(`$7nLV3#~tfwdqMQjj?+ss-KK zZ|$AnSL@OP?8n*tkFhv~=`%d5SFU$0RRVjVpzY7qZ2mSrND>$Q9~sGJ0{h6U``145 zKd*n`+w^%8GE>`>xvy;7%)<^p{s0I0g&DBPXt0I?8I?_L@k~ie6YQG{T8Nu1P-teUY+Vg33RFp!?{XRlDi8Kmp%6phFIf zzMk>9s`5pUQVIID$yY`g;pa>teC2!dcf@LJ0YEzc|BC z8b=o{Zy=Vnq9td57zcd$ukYwfx_=D>KI_JRRoL-)eF4^V@?V-ZsL#$~Xjc0zDi8$d zTMP3#i&@1=py4U7u;RZJ^${q_$I1bi>F;2i_5Xb1QEmON<9KawVptlQw3Ni<|242q zeP<`6dE?K=xzv$E=1hvr?|nz4M-r9#@omuI-=CxlN&FL1Oya|I2k^HOY)=(~R9pOf zUr2wsF;5TXwkdzagw-#;K00*#pGMcx-T$>mlnjo~=Z&MD;SjNp9zDPwss+N!TmSf@ z1A2U?++oY6f&BKvJsk^bmeAv?`N#i_-97CK0kF+Z|91lySq1)omTVH1#jphxJ-7eM z{|R-stN5Ut(Z3vCRs<%4;pC6Kz=!{Z39QADC7b94>Z|2=hf|gp6%L#I{;!_%KYI@I zK?470_n~B-NJA>2iSDkYEI`oA{hgY^3C&#i3#vdQdGe;8z4m16yE*uS6;fD1Ku!inT(GDzS-{sxR7 zig(}i8Irsx>srb|m(J#1=%0%aLb9YPdRzh!^&uS<{X#hgFvv;ob zkpHmbuYC&xyR?YxEQmR(A;{Y08#{5LW~<8^OP(QCH@N5Fs%D) zN5Ifs@NhR0oq?y}31Zelu3EU$?CoQ&P^C`WtH42otS@qfZ@L8Vam+ z>HY8e+ESq1W?S8!Z$>My20Ga3Sm-l0ec7el^R!lP;IEJ_Zn*+rOHa%&nn{M>J83MB zX);@MIt>*VE#R6%+SY}`9N>aW7S+5 zv#Qy*<|P#0u}(cJ{`3i3RP?}_a1QrEvWyWxvA?B9dzJ=DJat~@z0m zgmf;h22vQHJpuB|jmbQ>_lA^JQ3pC^W6BIe4EN$wTY~vVK(6r!=%-|nzri%utSR0y zkv4z)Rxi^Bm2!DJVaY#`3Z4&^|F+C#5nDc$ti^O*fiom!m{-D3U4+N|u4m#!1%PYa zqJHQTDe7y%>led_l+igGSH83fgNGg)zWiB4CEiXaEiEmjCWl0g;#>Yt=Z8-ibUx<< zu*7-iV_9_2q3PdTz}gzI_FdD>#>U3ir4uFVD~|G8g}8MU@1&ob%G4{k^R(&?S(?(} ze)C#9r~Yl@Xvl%&wn;Z9)7(!; zOn7izT5o@UcURZP(x!J-mp+I0Da%yDeJW zb3g%+?rCS2nkCrGKAi297pBN^X+b6pe*q*;y#-`Z@;J3^yy@Mf_nbiOZ3FesVtkzb zX^B(hH4r%iOeLfC3kn9;4u6j@lHa#hc@_DTyr10I*W;C~ArV+!)1tnxOZb(iH}?qe zqlLDU8d>h)yb`D@h*hnmLZzlNt6Jx13NCXeSJ|M{=aohYoVim*dblAHo0wsCwov+b zrKgLF#HuIces}$Ikn>d-2I$%Ap%-{1Tx!BT%^9>6p*x*y@z@4Q6y!6SaDs7WW`=pP ztZQFe-8ZhW>sNz&{HOzSeLbXaBh!4dZvc#|%OANz9r0N62=gRmUvZLa`VdTQKT7i; z&z%C3(B`WeoTqNAwTop<$@}nQo?Hv*37e$M&%YFm@-#?{o1*)>-eQb?97QXIrsjQ8 z+vOb^w@*w=Y^t{$f+||CYeCK~E~<&wudlY%=}dNc18GzyXoOrfq?(KBGI95~f~2=6 zOpg*KG$T}lbQ@#aXm5oz72bk`DxBU1*G&oQyUV{EE*uoiNh#Ct+>XcNQ&dGz_2#N5 zkj8KUkZiLSu`!a8Vg^=GOz=2YpjFtRxOe+pe;Hs*?D`bZ)KZe$-NhASM9fs}GQVto zXGh0m98pbh*4z>zt@s`Us0C|+T&b*7S(T6@Wle`)q!H6lz;;;^IKL$|8B8v*PTf%U z+#V7#uIgW_LOL((ZniF|s!+GvXK`-NmtpJqOiCRW7gunIh9hz!`%E{R_Xl+QR4+1Q zjTtiMoZ9A7%$X2Vfze1dTixO9;b3{+!LNT``Hz-hu_QT_Xd-29ZFzkJi$u!FwWIAj zC*`U`*2!$`X_?;&1blP4}h!+wRV)0fc1fs^wRRN!{J?6e7Oem!F zMqrkWXaf||d9q(+baC+Uqn4JIvbO!ZAjn*>q@={m%nVmAJTDq{vn6D;sxrEC}F73r13Ir?#Mn643=p1=%6g-FkX@z@~%VJ3hO^X6Lj+m_O=hVixf! zhT|&Xs$qwD%12hfPv!WI;&795{aBlKZ8Y_c$c*%xsr7B*wT%^X(u z+&iz_YkK+n_vY^SVEa0gyBtuIm6lDNhJs+w5~e7nk^ey=dXR*Kl!tM>y+2WDRfWY7 z=|VkCjg1p=b!z;xhb*C1IhEKi=}bB>K`^T~ABJ3+$rWCYreF((mP_-zyFQ!*yqr0L zjg76gwic&`M#MDM;69a&lltB5kbRd3f!Qv3!!<|acygSqx&jQK&-RShEL#G1_d;~VqE0e2vFFs&D@ zZxs+!GgWUq8Qx!eXZL19sn))GVL;4J=9!zFjf*!a@P9GR;a;Y)d?Svk#mfN5=PQ_=Utle+w#+|WN(kX93s`YX=TiAI~0q_&r z$EJQ#AAF1A+n6>Z2Pt|j>!ZO;A?Tn58wDV4^mM&(5HVs4{4tL#*T2+Zo*w~f4-#dG+Bm_XtrqW{L!-ms3F>AdZcY)l~?RGspc(xlV zlk%=h6ab9)&+59mx_&CvQ>G*}v~9X*3N6Uw1^GOYqWk%tJT(S=1|Lg*0F6Byf(ZM+ z<)$>Du%7#Zs7uIGb-}}K{ojoW5qmS1OBrCND*xuHh(%uJiK;7MZ&gJkyz&4w5ph=o^#rsyHue#jpyT1-0|N5tkq2+#0Yo5 zQVD0us79H1)LpREJ{n(4YzY(9xOFt>39!enEu4U}*;9kV;ebhQ4_V!+vBqUk;8ICm zb_bhE+R6FY+K>7(dy$f?P{q!lJET_v9q}PmRx%25a@QqBKNo^nyt-PfF9uJbmVKN7 z*LH~{{blv$m3hjE)z{Ufi!Ck0xkmNR>e;drZsXpD8%8HJhpFxD?SpnlAV~A;^N6eA zHho22Ys;>RcgFSAH8e!Wdu`nWr%ne&U$C;azU**G_1InZS2af*0gQ|rTf zI@kaJ8kP0%A8$haFumEA?m#m%smqpcdcXSKG*gZVnm96Ll+IZ+Rk8Lxx^0rQvN&!( zyJ`4Gv*292)Ul`}S5`1&8={m@?m35wThP*q(z)T)Js z7+Hxdo_KswL{1*6 zK@m+r%GDvO*Zs*G8(umHUDQGFEC}wsj(YNUehCQHkGp(%)8 z7ATUhuCK&J0*jO?uHpX{`hlcC>ZTN{Dm;~j=w#}eSSbi#4~nHf47ojNqfL=Q$KB|- zsdL)e))u8dE{Wm)Xmo6YvrGy*2gjywcgDrMIwvP}qN`e+*6PaBw}hi`*#Imry=(kA z;Xik5uqgiUO;`hU0Znx3u6pGK-YLS6!R{SFT#15mIyTiC3c!lrb+*NZh6eUy`c=%z z)0U6>qe@?cR1(ksx^e3QphWPfQGuTq28G*+^jj1%wPpncsDlG|7^< zK07}6>c&y8-4DNYsp&*?*8yfP3~=IEo|DmGb;bycS*}@mf(bp?TBAaJ5+X zvAnrLu!9PVQ&(itjwR{KZ07)Qb(OKs%-AHRsjbpn%T+X!{@Ep^StcJ@8JW4>qJuC* zh*$D(t~^My?JK^GPZZd03FQgGaMzHZHr9VDHwVz-a9J5W8SVXYf`q@ZOgQ=;PoP7f zK|9yVJ9x6m19PwD@Kq5->St92Lr0{hU1QuiSI?AO^NuE8|t z^gFD`q!HMv?BwKV;%*<~ngo-|;Y?z7)`u(>g_hkug^mG{&(O#GAN6kGqyiQ`@D-vCb?NmnD8C1HTF-B)!}L{%rOIIO%&%7*2^RUq>ZI zf_=*8S?RhGtDLsT1hXhs7tIpuj1{Ada&-Rsq)jSF*dlKfqm7fl6 zK^y9}v)?pSbL$7_xoB7?t`CRrLn7n^LoQCb@qXyozHUw;sHz$xiC}*owYc|@|LxPC z{5vcpE6QGJ2~YaHqdtj`E~Qv?xzk^eyFwz7>dZ6{lo~dL5`{!_Qnu|<`(c7xt@s4_7==aN}-x-O zu9WmdNYh-3UR9OtnGUWdGHNW20UEfsyu8fG$!T-c20wQFLALNda_t6tnT&U3zjoVA zQH5Mk_%IMpX`2j)l;9$BIZCvwq}{l7*u%!rGIm(Lg$XH8>qRb;X!d1*St}o}m4&3} zt-VDawAAtVtE#tlQ>%0-=8}!@yI*_vtKW9al>6ey46Kd zV*hFk>ndm89;m|d6by*GiVI2r0_khW6!$$Ad^3(o^G1iR1%~3ZC zX3=!0!v3ePqfE?)++%k?Pzh@m+-OHf5?|BmEy8?@s!-WtQPbyXzbcDE{2M7r)@3r- zJ0L?4Ww_~_+1t~DUM4+gC$EVqWuyBCwK!I7drONxf39xxhM=@mrU%Hgk+w>%C;5Vc zL*Uda%kc(EAPl)N5(bcqnt&_Ii$wG%tWb*n*~tq+N;{J6)HMr1)yNiu@9Fbcp3$jJ zUtG1!vZQK%kJ&Nx6((DD>pU-XAkP^~;(Gc3Z(FiXMtH5Be#ZIbo6CZhY&U-*SP4NuUPXM4)02rC|Q-qfl|UhHg7 z7AZr+sd9N@UegNDF3PsSsE@K8r0uzEwQ!3F|1=XGDW~MMFp?XpHB6-%eRj_f$jVUk zoL3G9TP@(+HKu;=wb_>09t&f%v$J!Qt=l?Q2mRlPl7&%2Q^>!i=inHWZm#Aw5DLOL-KzUO+IbOcYFo!y<8j{@hL0j!1 zNkbw8l_A^V7~sPGs?Pd*-9xD@f=cfD&;OIoOjvDa33HcmO;LIm@ zQx_xMavsTtEKal>{1jycUZG*=C}SfI*QoWvh7;d^hqL;?T6cllx0*HpM8cA(Ye}lw zAP#zEWM+P7I}xd4CM_5gkMt1f2sL0xh}^W#x_no4VbC|gKXdq$y{oJ1N3?c^gx_5^ zQhCv5r#hbN5cE;`=YvQx9*w?#e>fZjTbGcKkm`$}4z$p|w<-j%knFv(9|W4Vo(Fv9 z!7@w`RlrOJgG4Wv-a-XY)r6LkF4AnvH}ZMnbU-{koXA;D@SJnd>*>kPSiyyfVhI6G zhsLRg^%5^{z6&sYda*9f&SSYzb88#ntvtwfmI;3@A;2c+i*X4mY8x1o6*hVz&?`ev zw6_3^^4Vng4*XD9t^@58Mm@!&#KT>nU_2Z#?E;LVO`UTb@X=ZPqJY`rtmoAN1{Zf# zDz9I2h__!D40T&3>C}-*&z84NFCoZtCB0<8MsN>Rff@#4$g3_mlZDnXI-{<~?}IVS0KxCME{h9>;x4%jxg{24`oN>SI^5 zpotA)z|!vfpoJ>Zb=_%=VxH=OCv$OeF0ICQN+_+2FJP1b+)%q{@V2b*%V4FiK?u_^ zpjUFOM28l@0Y~@^p3-iwQ>iL-k?J&jsoP@s^bZIcDBjEI&x<}%BA*ZT)H5Z373_-+ zgWF^Dp4O^_A?4+d{kZ25zZxxYA-u?e3=&#pzap>kV28nj-JHwhYC&x#<)U8D;RR$? zMZ4-PDHzh&C^?+2lcA?Q=`r1rsfD-)7L&!>wD@yy_0hJ(mT&LIeL;hmeUZuNHlH_5 zMMK1TQp@%VecA-&kd(Kvdwhz}1zW?eL@?61!iR$f$~z4RyVFw*8H{fEpfWVBfkuk2 z`I2fC!!g6PW@ev=i8-W+ruO#R*=K}6{%zVyAWL}fe_7lwcISF2qcDj|dE(x;D+_w| zaM`C;qSIe|X?;~RB}LtR4&?@*CC$viQVrmS!}^}h2sybOY9|5Xb&&;r2#6v_$7G2I zy>ugvW81BuuJaB)t+P};7|3*2RkK+;l@^ThRMG{|8l-xCW!>j07P~)qMj}H>Fvx*N z@VLMRkybVMat&Ak4CzqoKgU|k4=r0L?8(PU&bFgXDO#_QFwr5&2mJhn7AE8d; zdiq2I#1Nk`snYpo0ik7UUqOt%D!5k^!o>&**w-egZ=^holpKLeVrns z>7Sunm0Jg^U-b)>Yulzy-X3SZe$;h6)NB0%f%j+R{5eP@ctlFKWsY_|L%%BkNSCRI z(%bMeG=%Y)dohJkHj#1tUD?}Yy?CCl(rIvi7(+j%X)$w<#?Iq|004(kh#%5piXFFkx9}GCx;j@&=mEaGrQH&RO&vu%a*~u?+eg{If7TH*g6V2qig(_ zE+z29<706=FY6i_7C(w^$-aBofmKVv_>SN^UhE~@r zDu*M7nPzdcn$_xQCSpWjEBxMlQ<~_?4hoh<@0l}8BFp-tvJc5rIq2Oe6oPt0=9q3n zZ6NO52FijWD$E&B{<7_X9ndp`=n;@dQl^(w6wftJ!vzkX;K5x7%8=fbi635E&VnwP zc>C0%O2}PiJib82S|%V8bwobrfhEj^_6}s+lm>ILt{pRs*T0z+4d|%DCSu>WtJ9bu zWzo0UAijP8we4R<(K#_JcDp#_s>}qJJr~FXY+Cz+t}|dcZPUO_XS!pBSD1ZWNl$7! zr-s8Q<3G6q4Tmz^fb~R#k!-$fuJ%Vfg0U?*`j@7TkA(H(4H zcDn-i%u7>pK*BR2^>jfW+>{}KXdK_Yl&HaUB$m)w02b)@oRdtYOfQ<;)Mw(3br|Os zEI%&MvWRF(mxyjk*#Xuwo75b4>R3hRV114FkFERZVVHM(v5B1dlk3sIZbF!sEioG0 zr>ysI{{~Q#CkGZLdSAc3CeaoS6@!VGD?E7^{RDX4g?i9C_4;-Fk=zp3{*Q} zj+AqM17RYiCM$6Hf&=4*4Pfm)APhx?!8-+%2`v+&M0O$RC=2(=IJ1^m<#N3(A*ehq z0u^QF1&bq*Yz~$guv`;u^H5#$D3K!K0lx0wnK)dg=uO1lD;_YpE_t${m$&0Q9?hJyB&q%gZyE_HK1Bz!mtU&4HwJDt^4ev;+Dq^D$dQ267MS(tTee#@Qa#O5h_K$)Xr8%{0vR zb*6_vg$YY1fniQvlLpP0xL`??xbWuKGjsT(*-5A29u+H_s3xFSsGCbkVYIdWJDjiW zlj9rUw@0uU!JTvh6|2Bs+tPD&;V}N#uqdB1cOB_3@9N79vusKRNqmBpyACzOhq4U< zq%IIk{xacpr*^T55mpx9sV@Vknpynpx{=7Tu zD-Q#hhPl$7w$hV5sS1W8~JPo~00zU)(NN=?44r9f${&Uk-tH z7DHu<0Pd$nWio8(L3xjB#uF)~DF6c5WfpE;yO=&RaXBtvbJ-YE^eFK!DgSX-w)=D0u!9AS&om>cagDn zx+cXcS@c2f=;38BezWs&0$efpamqI3pMmFq-ELl78c%!ZN4aVt z3HrZcerQDiyt~N-#D%S-!vq0T%uuFSG^a0}l^Cbz)F?OpqE@z=-a22*0;FPtk_NR= z^CP3o-Rn?@?K{wF2kesF-ECdcv*nN$=-BPKZcl&BlUZN@POpi(A-VBJ;9~A#?O2q- z&E1^fHKr1v*X00i<)^FqOJhZ>uVJeg6dD1pT0e!=Df`orscxM4$<7el1YS`jnQ#VP zojveOpVzL`@j>73gwq>KCwAqNXI^(+cemh9x^XK_PDNQn0WXhoT}<#=%y~^KHF! zb+6n@-dGO3vXS=|ia=D6F@EVCJ3hH_RLqP_gkd%&MUgbAMWW7wHdk`dDvc zIgwEJyq({refSQ(rzv~Ez|QY&q-ZBRzOgzegnt_`8Kul6i2dAJex6P3c8v?Ojt^fm zdbDYMg4&5DUcpN%C{gDnsFrJJGMd^-SlQup4Tw77m1AL~sO&-hd=U(j<{P1xbwLfG z!PMba#>RN>Rpdq&(xtrxUDJqP;YY94dgCX};G-F*cfv+I3%pE_2-9S&GMztFmYAGC zBJMfIO%f|5HhzYr%K4Nsq-Y>lzGJ8d+Y-G%rZT1iHbpzXrF+Y1Vcec@b(8j!f~Wxe z8U{T`3A2qwtd7;C;A6`==JOmS)}mnHvy1nD zm#QZ|f8GhbHp76{5ldoNqy~~wK*ft#9eR(#}uYRbe2yKiaMSSJWg7X7DoPqD6# zaE=5g%n@K3PDV>zJ`2%ToLGbdeJ>?_9p;_nDqmS@(?ISj-0u6cc1~jB5&X)NOK$@Y z%m*xuwmN*hOkfNB_zi@kOXvk+_LkE=_ajh%sDFNsaFl*eO*F{TB#sK91GZKqJ$#Q4 z@h<@zC&+gPV^If)uX67|MR25hfyjp1Z4QMoiGU5q@o4j6bT-45ab{zCYn|A5@U`-e zb}b8~P0np8 zj?8@GvhdQWE;y-a7CuAPfaT$ytnl_XH-ucg8*I6O%ClO7`e4Q*mAYve8}DRKD0lz3f}h zeMz8niQhY5cnSC_zH{AbSCT|cz^i>^I{?VLRTcm+PbqqypVmS~cOvn6I=fvD2gEZK z_77~}sTu(6pME~GO?mT~42diTyYn%JU)D*Fmy}b5R)AN-JGdX-eSLnc549md{nV0M%O>Q#G~$N> z)Kz*|StO&$qY=#Hfi6#40EYIK<_DBh^-cAGN<5Gp@7OizvWb3SCvl%pd)(+ewaSAm z#0n-+7f4uZ-!Ies({or0yCd3;kAJlxvuE`bDF8LqeftTqKLE9!NGm-y+Mr#uHeW)k z#>`}90Oxl)kaA0-qv%3X*?p$(qp#`5IT}t>oilNKep)h4)!^95f)Ir$yT8z5S`WX- zMk_4%bQmaFalwJ+_P_LO`u(H*N#Z2R2n!sq6vOzijZGir>xynGVxJ?*8HMw?<4e&&lj?z|4Yi ztQ9A>lNCr1SUf;LR}7mv(+(GR&I|3kT-tShy@z$@dxe2}Lyd>lo3i?tL6ZZ?fwt4s z9ld@QBr;lXf}~dO&8?rQ?r0Sl@{+h}RBzY%T-iSVh;!X*;q5r}dlO(3&LIa{@JJ_v zJbo=KGWwbxfI2ohoTw)##&rJz_0v3C>dg}{Y=aq~{#f#@al*Kh!D?<4lyj0ZL8r8_ z{jh`fm3SD~YDms;4P6bJuT*^IPd->A_Jz{PVjbpmxiD6YjxV-|$Hdgoh zlzx5%umtWQpbCIM8Dbqt+2OEsOsnpAarm4QkwyAve_88ef)9}?vwf@G0K>u8P1;~K zrzgBB6i$Yk4~ITPbHfnwZ0-Xw!1m03Kac^Y<6|=jb3aQaRUNJz$_WiS=_jpf+(5Su zh~Ha81vXL+!xXsvaO?-~FTz@N=^8L8i^$52*Mf8qo_=%VMs1WJ zrgT7G+J^ga_L9!oP=MQ3Nn7?Y!zVI)I1}R5xG>BXE+D2^6grW88?pUszNdE0UyHW{v8KeG?UQ@mTY zB{h^Oj)q>*$}4&DZKP(LvIW2ifH1cWdeQz-51k~|1vKs+e&JoPFRGWcU+ACQ?LemX z?*zrM#n(bKvJPqm@k{3$>xIGx3K|x6A`bjh4<4VbeMMP8K1>E&QkPZ`0C}fDt2qDU zVT+0f;E0Mj@*!c*p{RZ#R7yx7IM4$+t!-b2Jk$oD3=;SUNw(EzQYM6;@1stExl|qZ zy=rqWIOL@c{G{B25bz{e%Y;6Bv26xiG%}^@EQr*Lj{P;?E1m^WcR@{7wmHSWj)DXN zT;?I$sDw@eN^E)e@Wp>HJeL55WnF#K1qRK-4y63Ur`b(emb&2(iq1T5^41MdAM;;S*c(h3X8 zxwT)soKg9*j>A4fhHnJ!17R`oOMbvXD6d`i$Qxk|>HzJD&LCQ7wT|s++Da;UHcuH_ zDV#Ok2gV(JucRxlmdzJ76lmmiO2m0M-Js9!^vjxUPHtN zVyFoW!o$e!X92}rDBE#53uLDJF~45fU-EpZOwo;qD{L-wn z)n>zYG@hLN5E;zF%n()j^SD+9kIB+m#u}QYDFPF65!BD#JtC|z1TpJAk^f$l)ib^H zv}(}khimRd^yRzkV zuSLbP*Y35eNpo~{bs6y)v4F*Z9GsmD!sbn%*us|Ts>{2>pCBIX`b_X6E05 zbll$l{&p2YUVcdT4!O=l_AQWYe~g9uX zF|@7irjxIwhewZ-fp1nQna+O1NW7sqKqP>EdP&E1SOc!-*82G|U8FU;-8u-`Hpk_a zBkMfm0cw>{r(D(^W(Yj`(H9#>Q^(F=?JrU@wyc7^WM*luP*+a4j9| z!^CTgEAGx8Lnj^OV;Yz%ByZ)8=NH(7{!yi4kkr$mjSg4O+)`t~jK`whk3Kh3N-plG&CY(_Hm~=%@4k?b#s8JJ^8!G*<3x_l(g$S5xkJtTcQh(yTf{`r$w*5 z^(il;ApR=XoiBU0WIU^cddCNKb2YR?sn#mFEa%(%`rDmlsQ%fOs-hY-h4!)jCp(v^)8oy z?hJv_7&$?Y-s$M2;BnO;DxtSX zYCk7SLSpI|XkM}wDHiajr}2DnD2uIh|LHpeiknmGM+#KW)oNbTm9}axt?K8K=h&Q2 z{K58p=Ux*jt6l?a*%_G6oiBKO-h@BHS;+{7h=u{Zk-d-!&y~|P{m(chmv>Hc9Hca( zJODqQ-J=!6dx<-^>XC5HV9lB7!-BMn{pAVRU8UJ&As9-AItlU4O%*y_x+R`_8zi3C zV4O;6`zOVAU>Je6lY+L1D8%4EDeR?hf{|Mfc#gm=R~X3pxjkLrE)8FK&}hy0#3S zwD#}ck1EN2Lyy>78R`9XEr#HNE+^2Mscq^$_(<|Trl{WNE3G(LXAiV}l=)@VAx~Tm%c|hw1NheA} zp-tPj!TbyEj~GE&7ZAXY#UhSqWt4Y6D)8zYOV(x3$UDLB)-9kQDW|{Ukh=w z&jbXb$F`TTxfXjvY_oVbXBeD~j|}2R@q)WUn|9Aro_hRR3&kBUKbws8@e2}kMFGOv z5^$FRFeBBX^MXE^;87)oqB*PHG{6LzZ8f&{`)j~5T;6AT1hS8Kw>g}IMwTq#t|H}{ zz$(A~Sv`H7eU7HwXWADtfb2{EOqwqJ62_MiDw;B`p7;N~u)g4F0r+DgXcf0{{PlXk zN~s%K0N~n?%Pd*+>h}zvl*ze$0Q45huDqzwxnJk`tM(P>j{%%0%MZWvZk^xyI zi;e1>1r#y|>5ry~PHPr0?;{g_<1}6Q$7dRjW|w8a(K;q3X#xfSboo)Ox~3k8OMn<2 zpEMyZ(#;keHp3$N&BZ@#9gJXIncp8<+H2L7md(H|Su_aNb^QDWRV9xrcB0c|sBcGE zAT8TIp#j{QKy0oLoBr`MbCnPJhyJ$evp;PGxi%!y?oWPv1x6HoB%d-TFBiE-VJTLMKL_)nJ0m9O(acN8zJ`57BCy=2X^>z6a07DO2R=or4hE>d!FT2zvd8?nIKu-|7Ky;#0F zRlb_&3wI`+-U&v@!~&iwn&TTCCnLal0|b>+)mAGM=e^p18}yz?3^F+_E3eRwnsfIm z8^Y`8h1on@o@p5|Qtf|JmQ4*2qE@44d(fQ(U|r^W0RiK*mPY&{r*bb&c*{2stjmWs za3o?A)kA|8gK>*n>z2MUp2ziG?<-B4_vBRB0i?yS#7LmkJ%u)g)FD))x}B@*nK*Tp3dUPEwKxnD z(ogDEz$hTxVUFX=Fz+;1Iq|kHtKE7XTdViQ9gIdCc`G(mph%T2o5Aq%*F7PK^%oU~ zH(8)oZ*_HbfRW6X1!}%rXQ1+xn*Q7kfzpp;QI;pEQUXviE%hm__TVs=l?)G5uh|ga z(_g$bk;T5R=-w4e2>d(%C=R!3IAhrLpDJ6Jcsmbi^px2}`~p?rM$ZN;{F?-f^c#O> zD|u?b?2TAe54n%3+`T=8R;g>g-uJkL5o?N)2C*m@K26IzrG@v_NMP8WZ*#dms%f0j z=`m# zpXeb>%=|XxE=ns*3|e{Vn67(O+fs2Z{$@0($3 zHBDHa8%Ij?4FUevlfp|YKVa)AUhxn+j#>?q#SVMC7rQDFR#v9O@bS4`fsNA=wICZk zYsqW1F%*eCv@}nw6+(SnDkJxYxzjsI7iLJx=#}|FCWh2~68mpI(PI7?TLC!5QBzy5 zDm}d>&jG+#ae61NVhQAR>{wi7kb=76B`_85!e8k`?E_uN3dv2f=tb*Y9{ZakVXbHajz9&e0;yYvMvNE3Pr zY^7Re&j2XA>Qz?=4u*wZn5XPstObc_n@GQ*n_l;q>-EWkuV zofAN2AiW#KfulXAQH}H)0bWj*ZU2!CeKk1}z#3v392xpM>YJ?m7}0`Sb@CuOuScn8W984WQECh7)DB z1T)djsPq~R(fAb@z1`eHUkfN}$^o8=^W6fRuIIxlF@;qEN`IhSI35ev7*E0g-W~vW z5a%COox0^#!);SF97BMSFB2X4?gqd8lG+FwJI3fZRsmRV5@m=FY8ht+?QeR6u$;Do zGz3%xYhdD24ICxfOcUbLlR<0HRU0jMIxutxiOp3&1RgdixdNX01@8egoSeDYnwtiG zs)m2|m(=M=4T|B&55(s+@Jmqij0z||I2*@rQQRF{;mFG;h&S5A&;Um6LY)MyreEMn zB=$E7N4=1E10P;3l7>kopnG~E(+*G35}qPRhv=CM+)=P>`fEP)8XfpWh~EC=zl9Aa zQp2ZiApkt-3Hvp@qJFDxK0}5rBwPx}_nyM}9({J8>;+0b`&BVQBOp1lgJlaRN1xsA zUzd$?{H#M2M@#kD|Z3Exl<@oaT;VbF*9M#upwB$ zHB>My4LsoA9(^Yd5bKH9u?>f*X+dSPzkr*sgFr$A(~-IOE<1D$;imOVO93H3)<46 zo34U`c%QoC&OL-%Clj5wcO=$40>{b}6chrfnHM(N(^p64HVsgkfc^ms3)@&HE_7punJWx6hqQ&@~0;Qf=0&Fm-Alaon#V zwXF=CObMj^b58mxI1`e^q&y986_6O_YA|-d$yJzx;iHCB2U^?YE$H;EFinngkS+_@ zo>YCAZayE+2+dLuVQt@WacI{cISU2|Q`&G=BG7n3G+TkcO2DsaY<5vLNE{ZcRCgT`E#1kt zHE)Z%^Y_t6`j2eI#8>T{dE7>uzhZu<`M?COu702NqzE_^-LM_hRmMLuE5KmM?w`{; z&3BMyMn(gp9mD9&7nh?7Oh59}IRAd|Xe0`xWjRhJ2sTJzFf1NQP;1V;d7|9$wpKJ; z&)YKsP9G?3-8J*Rb9!sVlfo=9NZdF6qN3HI-&{bPzDL$xt^J}`T4F2e%vfDLZ+bb- zAy@O*8N}{dv1LEjIRGY3&vI@qCBDjp{aW(sNN;(VC(HB%I680lPyF~+Wwiv*$S}hp z-8U7RpgBYgS_}<05(P)e!R7SybjIeL^wRzdxLV*O$G4HoT5<2^ZXfuB*#B_#=7CW5 zU)cDdXr)rgUQb1(nkd;#yCk$IiA-gylzm@jv?ocWl691=6pFGlN|AL!$T}DdV;N&+ z472{ut>^i^@B90An0xN~bAOg|u5(@2p(dYSWQ|Hb{0pImtT_#2LbZYoCRCw~#6=yT zf{J@cBW%$5L1H^swgD|bt7y5N9*6F+eEu4UEah#og5>){$v2Q|G$0uru+!ZR0*rp= zZC+{jq6M3+qWf5w&(cADUJV@|&tN6`;Dq`4K`iXf#KaAi#Y^OoGk>wz6-5|pDCQ$& zm8?4W^8#%y(VN=np}YE{O6xK=5a-`naegt(qF%^CYxNMbJacwaktj?Xwc^cAr`|%( zgNdA>e0SHAcy^}PTjykj+o;d;*&F1D5%dMkE{kC^9OIPF=6s+_;hIeR#qd^;{=BvT@20#pko@FuFkw@* zD@wOZYT;i{YhvJx(Z*PQRncV_OScPuiwQ9I5Mt4nLS*0G|Nnf}+Dc!$tsBhcbbgz` z9@RHrzCt0dy!?C>L89-0{V`()*kGu6B(eEFexA>I%dgpmH>T2d-<#q^BS?fq*++oR zQNFM**Kdj-D@1P40tus>3=mIF@&3%hq_SCD!oI$p*+|E%RCGzYvh1 zWFV=+{_UUal3^Dl4^_t8;M?C23%7Xc<|6S5YNq<-mQXJ?amn z&fX2aH#J)o*oHidHQX-xtDHHQ=OlQ`tyCee`@cp5qD!QjR=oY+0fJ*4(Bqd#NRS1S z6V9IOeSbeIezx=NBU08gn6!UMMF-(?2$kSWIus8uBK zKhG0cP&evplu+LUD-L0Ajfh}C^n@gj@zN#l$Y16U6uuec$XP1`06Sw zzj>x)F)I1)-#>y1(@W92Gj8G{mCpC?r}Kk|Ih0a<7ybfV;1kS(vHGekx3=k9v&T2Y zcHc@+(JW~U@tM&O%@Af0_RF+n2pcPC!p<>8s_e4>PBq+tQMTRd#{MQ}@mg~8(6hQG zW_3%Y;G+$#J(KU~$|n|N@= zT=6L|MLuuMhKp#ZDT1`%W#h{_d2WJES^^k{ zneZRrd)(`RL>%DvW|G*GW8u~4-%;}Z_9`YchtHMR1;DOAexTL*n9khznZ-nPzDFx0 zkxSW>L>KYp7rj1ez(cKD2dovSJnKIr%N$Z`kCC#~S*Q@!rHb&UBmq<^_C`t`Ecx3D zh9TM(so~fYoCOLM7kMw?0aB3bw^*Blb)=)k3MUX861Na%W_aQ;o1SpjUDt#K1`yoZ zDPL};=Td)Z7+9xl)Sf$8?z~oH&=EEd)MS7__XYr~{kSoM^5BU?^ddW*g7Pu2B|$0t z9mZ>=M|JnF(0wf1vj%SQSz0@9hRnqCh6w!Id3mxoBRo^pf;3!h$->0ulf%rpD|_OO zt8JStSM8l%^h@P;Q?qXm94<=IQnzn{>NWoaP4n(kiNH@q*Scq)&D+Ph)(a+V=^_H| zT%XqO-F~67@woUgcAEEJJAoK9Rz+2{S{4O0LVc^0Gv?OXeJLp^iHXWcO>*sD*v`Av z(G_)T#3|u$T>$?d!LWY_$6rXsUt>>@mf9U(E+@uS?4k?X@CW`y@G_g40}R)ONzX%g zAnv=K&qRPRIQO(0*3B~-7V=({$PihTn7Jt(kwx{y;_Xj4!UEBwg5sUZ2NHLlaWMto zkp;;q2}wzrM<#jY`}{A>&!)%>XNkr+|E2w4kULr=gJaSQIWmGaWSz~i;#~+h?;t3f zpH$yk88@)tlrG!XFX-i&lL0?UY(RXRdho502X~OtQS*BHEnBi+Ood+|V|xDtkJ{IF zQ9p6XzKmFKC6M-D4mV8oE$&JEWu|7d9{Ba&Ot5jDk7cj`#w?&>i92sJW1Vee$g?lp z9nbu(Q!JPqB@2I`LrshCViNhy1a2h{D>@vwJD*4iF6Z4BVU>ByE^2dCBaet*l!9QY ztE;Q|WV!H5&V`R(${l#ss`!wvZJgd$$d=W+5my+NFE8h@ z?rD(OJp6Hc_ca$+2pz1+*}aE~f7C3NiC#FC+~L0lian6i><(P=*0+Z68$moCr|n44>2BG+>qIkHy>i94(B&)Vcuw%%mU*`Yq%sUn-MThoU`_6!A-NxbF= zG6~FQ#QpxQ@tQ(t3RN!icFo3hw~$0r%3!g$a8e9{6R5n-+>3JyM9O;Vw-Z@G0EMk; z_g$zj1!YP|6=^S#!jY@ixg`$kKdyBqCL$-pU*_`@CgPabQW}={KX?$nWmslFY%FvzzasUC957-q4|yFi;sm z1z_ZSMJ7M5tgK<<{?|+xgVx^*i%e zSsu#UE8F8XAC4)y&OMyDcJLgN!5I{Rp;;T_#rjM zFP|m%q5s7h+K{m#$JBujA)-*9#~6bCW=(KNEK2kTB?b)>0wA8^8FvJ@H8paTY`J_hGj$SZRF4*$lkHg{E{cRpn`tJcdy?+$m)!Xj@ z-ms7tBN&wtgod4=Y1@JQ*cc0BcXOn?_wU0!hqa_fS8@FpkC;c`V5wy5K7)y;f?!Jj zK_>{Xv+Bz3dxpSYq5SWTU`KDAl-kyN_ul^*dH$)8uU74-zlh{Kj|r;iNO=uH^S>KqwHG$Os0na?1B)$n$^QlO|k`12&Ql|uRk@(rI`xpty9?1F- zNA3#&+sqgXPul=DW*(@qhkx!wCJ87FY=C8`R3Bhwtn*)pZylisA;%1nt9VVOMw!8klZG^vg17gCk86+!! z3_{IPtk&#l?pI3uD8%3}I(3wo;nT8NVjj=*ZaVr){{VVgww%eocNa(9RsJ6qfLolB z{>HQom5P+Dcj#^VFB5=-h!*F9aPaarLD)U7{g}^^vc!JPgDrW3-takKWpD>ZG3JGm z`x2VBZ>Rx+LK*zKj~FZ~fO>b^NvsFEaxgx=jtuYxMA-qzGD66`pbgK}Vscwl_!p1; ztEQM)ojmW-7C*e1Fbbg&&?*A?&qCZ_+xa>8(h+WNMEq}tF)ya+eujX17sCD~L7)T% zwF+*l=~nn~0x~17QKzd2NXaxaorftgtZ~4I^R!Vl$U+EtxdnMUa`TWY$Y2NWTtl2N zkWKox*^@`YSh%sjQ)M(#+erJN*SFQ*wFf?c6g56bbc=gxs+>TKat~2m%>S%;zvWz5 z!d!^4LZn)B_>ywE)adb*YaehqBgJm8Yf0a~{k4K=C>ExJRLgK!t)xpcC$=+Y70Ogg zi_2A4mkn;-_Z>WhjEz;Is>QWaf*uFniM#SH=9&W7L4Dl=b~^~sg&YH#R)8s1If*^F zVi*(<~ z580-W{S={tt#qEJgNcP{RKj-Zu-EO3)Yy~-gs7n3jP(Yi%G`fIK%qvaeFq%A_( zt@%3>hwrHT{w(nm8 zm52nAg+NOD>^E7AqBMR#%KA1W?MgYZtR-PqlMp^A(l)LPU4t;ibd-d*P}!2OXf41E z05ZoCFZ~yh(y`)awFsnz3d@i$-vM$5j1Uk$iZ19W%ovQWl>Kmr;Z)g=UexyxNCgN4 z1qDz9##z!L0pW69`Jps&1dCQ=^%f?YI4nw?7Z4Bq zy}^SQ#MUj7M8~FUw=J24O&hgSRov*LfOmw4WxG4NF7eajJWY4p8X&^gKL!fPT7Cu(aYBEJ($u z>Unz`Ub*Um&rg1&{NGUsDk2QDruJBEd)Br@)jQe9=-{)%9 zs7!=%{nXyQZtf9WDESAn_Bh3WrCA|Z%EmIBwnh?ci3qhTFhpRl10@znNXuiBG~mNu zE0HM>bh-gbF-W{}oU{f$9a;4EEjiCugD7Tg@Y%kMkLAeJ&V`!Mvs?y9>*2>vpCjj; zwm&#Zqn>6mtb&kkAxBAnRH23@4i5@JGV+(i(#x>>{_zHF0Lo{<-}eby$9iDe?{-C9 zzI8?Jtri!*7UFohqi`qz@>nzs!0afTUV$}wgnJ)b z(t8Z|ls9^gWZ`BHB_NQO+P<1lVDkYV?=3K#hxE@k2fa{$HCFz4eBwDD! zUR%~JKkTC3WKF&1!b&VGkH#4QAg~f{4PgQ8Xpm8QTKfp%r?&++^D+oROUw+ZOxR%f zm_MP)3q`0eFmOkaIDr}4l!LHDKa3l;P;O(n?^I8EqXH!?2)OAN7=cN;2@j6DhTBMD zlqPp*>B2m{{pa=fujpfZ;R>)^RZHW!zVRB9j40zcW#yl(fqwmUGqR*+4@6|i^ve!iAkSzU@LitoYZLmdV;m&VQe|XM* zwKf(M5m}QmLN`Rr^7SnW!k;#uyc=&?H%5=u8HfbOj@Z^I|Wa97J6yJxrd z*CecOK#-1;IR118g)p~NJm#M#3ISXl;S$S*3lXeq?bH|ELx_UG9E3eoAsQixw&M4$ z1RBUj?G^Ru!H_f~Vx@{jH|GmQW=Qms9>QwC>V>k*Z-N#mbX?eshGk?Y%kai6xz{8< zk8PIqf4FtL+|gJts3^X?Z#G&6PWbG_WDYxKf8UsqGszdhLtevav%}EySsmL$N8rWxD*XhZX2HMb~sov2FG1F5d=1PEL35`d=u?t^(xV z!aJ^~6RntHWgHe4NUMl0yxTE*E25Dh9wmBlURP{hoN6pN@6V7O$tavU`RXR7HJFst z1!`q%Y0kQsl9ZoyH9ABpZ=pg(RS9_~nCzNAS3^OfOxL_$DYj8+`ZDA>K?~O>0jm6E zr~xgA`%*xb{U6~5oN9b`#jZA9zIzdWL=+g+F~&xoJw6gSYk!;qSu);RG<6?WPyq;` zN+~<53&0WxLbGqzj*4P?%`%&a)IfNh5eV<{D$b*~R1CgBWGj=IeE{d&w5I&tvoK&v zf_R3#3#Li?*N%=@p_g&|NZH^;T|m_k2EPwl&pJ8P?&w?}UJym(5k+NXaKc4|3nOda z1oCtQdtok&T{I?GX0+UjHD}v<=_+*^9!R!x9<}$%-e+3$cDj)R7$=`73}6=T?bUM_pEP$Va01fV_`c=ph)?vb{Fyb zX5fwk&B-~=;m#|bE=F9f`b)x#gVu243zksx#>O^fN6dB9kcFSf{#=%9;O?_zZg-Bb zOeAvzp!W8|qbwqw^sR~5rk?%bgYH$liK&f0!nR!df>GaEQ`^{LdqLDl7NJXSzb+M{ znV0kNb19j?%Po{nZ@GF^e3vqT7Mj3ycW~&bO-h^&I);Q7Zy)8@F0{ES;VRQBVB`7d z8=+nPwXU8_Gdwwm7ml~SAO@noLN##4V#VMJ*A-6j3G6T0D?jDJS&-CxTK49yGtQC$ z{8god;RsqaNBpiPQ9QX^@+R7^@5XgOoa&r)l8{ZDZV-)N2`ZBprc1c-I=!N5VTY8c zPsWQXgR0O5uYIeaSyDjASsJ@dr%fJEp6T?s>;4xWc8rhX{#*&MtB95xK9{lyOJffohrfBQCe^z z8E9Fx4oR%DdSxs}iElt-a3Z9gDBB=*-3~wo)beVM03hO*D!h~<_AVLsP0=rB++4@T zzzqfhB|k9b2$c?VK*rO2`dI?6AQ;g+?Sc4enm@;^9l`!C60M(N_hyml?(R35As+1q z#P0|BDr5!58KhaFoiez1ejM^ff{D3DM^b3J@tZRu9m6Ho(mN2FZ-~ z-Wy1$e%NMSWMuBJH-6%fRsI5(45_@Dqx7cV?Og?K#g8F#wLb--&4JX0#p0nrQfjA# z`mPn7VTWSggOf)E65SM@wUY4#ODHd|%bj~v6IH7Gg(GEM6I}&HQB88VcjbVbf7z?< z!2%K@c@y@(FR58^Qtt_&AA|N|A(c5g%D{P%iESl4rJfGD>yStrXrH@DD!`K)925%8 z+Ma^opaRjXoihg4#MPRy$8UKXKX%sA13%DAqt3{9owL@~qk@{c#`G-vIq40@YJbEC zH@kAn-RsY9ZK8_JYx%DoV(T&=Y~S*`5c@(dBu1ZsQ6@AvN1qaV^&LGnZGU@5# zUZbNN$&;scmK>+kVMN0Nk3~iOhXpj|&$|VzCADN&vcdIl-_A%nsVe2PHk4G*ZDVXV za#4?WPd2*`C{-5(i2aju%&NpA;gc6(=Yv;?A`_xaOfsr@xIJ7n+pcoIsJE=-8q3`& z$GoGvIifN1-qv^sG534!4c!Oq|u7QUY?(JBQuJa^rYU``tQ(bm=u ze2_PJ)*ucYW2VQswe5tE=vAc-6sCtaZyV4+y+-&!o8L5YEY(x;qHniQdKeuib-6n1 z%ivhpmKCp)UohIhXs6GcEd*x$4MU&KGGlXs*5g%-8(IjwOPUdG`Iw_SsJ>>$=?imCD`Ccc{!)^xEK1%_BDOkkt7Z$a>VdpK)Wf%_0)?D_4lhT+$eX% zXWSFc8(1k?b9WxAb=?Z|wodu8T7lsm#B0hc=i_0cP1dg{_G*SLgZ8;5w&ZhQH|`N^ zGIi856}zP4u~IJ?Kc}C$Oy<{o@mS#}NnjR~bM=e$DG5RULoV5)F!!IQQJF1y z(eVj&RS}(TT8fJvZdM_#!eF7#lUt5Wync)>v`%|N zR%O{`Py>w$DiUUMk z%bLVPFOIRYPtKmp_dSDxRY*$6pP>2bx%p0}iY*hKR(c~c@VqrWHC0(4^Kyg`61MHbG#JWkiy@Zi99Wpr1OLyH#AN}2PA0+I zc@^S=_K^*?#3)qno#_P^wM-3OeXO>z#UgD*VnTl0kqx6g+7A%3+T50h!^ri%UEix= zff6SZev}>WZdfKI@mj9Rzb@vu&(afGqmfi8uwzgh+l;{2)&qTg`|x^+7T?fU(UihT z8tt0nWpnf+_X3dT15CRYaTrND47FT{)I2srg|VsW6z_Ka^yfxtmR% z+UsTxE?eMns0)dIe1GLXB)DeGC1@C<6+7Y_;Vb2#$S7zdaDFOd}T)L)({b!A-gcFfBtk__jP&Y47mxk%YT2k+ZIMBU)?Ij@eV& z5bXPC2$ck#e0GN7aehJPXf|%Wg;!6i9y1+%Act!jmU{kt$CfmN(Xj{K5B&PED8WhZ zTP+m9N6|0t^DnJmz+K)uit?xF{i#mtBhV*s$!-ZL1RidH`&}&`Rc)DMxHU?6*up}Z ze^DjDe1J*+O0mUU)B4StUVb-rKcvU)5y%YN119Cz(two~YcipZPdMZt6n@|gN?Y4fO? z<*}>&qv@L0hY-!)a8Q#W9Hrov5F8vFrDCM@Sm=^h)711hPGWEy&D^c0_)Bi?ft-g0 z&cCg)N7LjazPWosISBpL)sjU_#+{gX69|rvQKLRVYc&-jGYfdtk4)fzq2w+f9L|bY zR2=h6_pkCN$6q^AfEqoxRjcSQycPXOg>I5_-dsp_Ba5{0cO4+CYj)1=^c}$O(N8j0 z8zLQCJ_s;X|A;8FY?%?d2%;|}o;s*jpb5SS)_;AOeQ%iWYzRruUZ&KL4lN|^Q&ieL z>D^_@y>lC5CY=(@ej}BM$2>FZ;DBPWBF|G2?gn?w` zS6lafP4jnKn?ofbwscUbbbn%@HoUWHkIz~GB5)S0a$z&aNZrvkZDUqUxG&xxsxvE=8X(5hS%UC6g2e-hUNxxt&$R zAQ9*SNBCgsM1ecGsH~_+Zyjq$EA&|Fudaa`Q|WO@SF2W>1&6=B==^|7g0~g}JOT6> zt0`~iP|K3kw?fzmC1dyrDc_McZ02qQ_Z}j8bWX+B3P0PoKcv0fel3A^delHETBq&0 z3q8CV`!)XBe}Lf4-ibBmudlEoDT;xM*%syE+fyqEf!IFiOH*w5=_BVm#jMJAiT5o- z3h4J}`XAl+mkUnnBQDhqu##zLsyCLtlxvS&+mmtbPw)U^GXTJFp6%fT-KCz5om5%~ z*yKrlA07_s{X0`Ch2OX*S+fj09*L%Mkh^UjE#31t%F22?vef#LPElbv%Y+_NkDilj#!Fx z$eC_1(^1)Vb@%O&_fWLjryBa(Y?3OxZcNHi)(8v=+6L2P)$b83#&dVPdF#mw^o92- zr}`V0j-4*R7aAG-I&%Pv?2Z6^ZD$sEbsmxf#0Yf?SBNQ@>h*WPEQt7qJX zeQe_LCX~y&s5=Gb91)|ZN6s+(pu!1>fU(1tUWA%ipOcg0Y!jMjzEn1>In5C=KP{6V zd3#fuO#SMuO=cs%u+7*jy=si_+OKiX(D@BDORGfaiQB+(bybYD{*vDE6gL;}CVSDx z)!;|D{4M2+aBj!`K2m!0_^X`9Q?6sKF|>_?&7n)mw)I+IHKDlMf`H7a*bBCviK#uu z2Gyg#$!?nA%)o0_AlWlKj8qa2-=7RtyY@jXHkGu;-+f@$#5b?Ga-|;gYEh9HEpJceVUoMO3d~);3As*+{{}Ybfz>Bs?Nm-S2oC=e9nJb{>F%<<W`U_8*COVGTPeFXs0f6Y_SR6k z79<)JP@3?=mX=G@qoJ|e%(>;uz>)`=bG2?buXEod*kbkXR~W-agUN|59l2irU%>+t ztSab{4RjaTRGTSqa3nDyFxc=c*mvc{^ZXOLd{>6aNJ;{DR)+C zplAW*WnYE@1?$fj1-~;su(S2QtIeGb%bmNq+RRY6yzJ18=5v_fXO?!7CvT`Xm^Qq% z$UV$t&W)!;m1}ccY6yx)bEr4Sr5PNJGJAR%hYYQj5HzpBQEP5CB`Sp6K2%4Ia>=s^ zAX?thcf$+INF`++k(W0`pZdm(aeHD!QV-3vWQAHx6W>}`r5l+wRhNa&97wfuyP%_0*3``4G3=AbAHJ%g_x;jaJh4p~zuv>pt&s%@$h6+g z)QulX_o|5Q8UHky9OB|uj2MT?2r%k`4`EXm1~}n4i?`(1MU7#SP4K@^)`z%x9~C{kVtUfeyLUf-6yS%xO2NW9NcPlb5M zcT?<|O>!%~U=(+eDQ}VY-_1%9dbFS{Q=VEG7O&C@?AJSbWbB?kC_~Hy zRH-4(qimU<*NsK2Xs|NBDtx~F>5JA#cI3pVoRn--cL`o}WOfl4fxmqUgh%!CE_9SM zcz{cP;mxE`1L@_wB70IK*5F4l=kT+VnHH+(kj=9aW&}qNA&Xgjf!DiTYA!I;gdU#j z4@iphc_f|L?y>_*ra>F3`$#o#ywu{ilcTo{BdFI_rq`?6{yfGom5`*OtTO(6_nl>A z4gojQVWJwkM_$f&59Bp#e*{2xsF5a0?ZH34#R926S|}+gv$`toR+V%x3O~egu}{N3 z8Jtz+jp*;&JE@6BBU{wK#KO`-kVeRi0Vvnv*u3a)Gp z8{Vv|XCxSJiXI7_#(}AH`&hHAmBHEWg zMKO!*6m7I{3sGc!d3P#pa<)j;1m~20*=+-uA-s@QBJ8m zjZ2H5k}&)}31}2)2z^_}YruJ0-QoGZ$)1vu+}vsX(EUPVYns7`5yOw0`wq(An*@Br zuN1j~jr9QV{R$Ogt*}JnCuLENfM$&8ZjZS6S!{ z$*CXSS!LX9>{9shBjS;;%=0mInN7pntou6*N;e*zd;3`booKv+IWGO?gEJA+-CYcK zr$^|ot=)AX@i~nhXmW>$6dd zHanR+1-TF}x$Zea{5e97kK%lZvk_c5A8$uK#bAw(=T%z{Dh}t#xSUg_+*QBkB(%xWMn{q=JP-%s-fxWUcLPH-ZNcYw*Hg7 zk?y>erlF^*P0LbvAvgBXD4hCs)c2ml-7X_wi-pl)GBmL_cCgQKEzeE-$YkFIux+m@ zfL_^1(WbL*X&Wk`K>OO)A%NOf#7zYz1IcZDQJ2YkI>fLLU)0MyTYRXSf1BTPKy%$P zXo~xyRoe}0l?x5$)3%z9(Hfssp@qjnjz%6cDY=c;Rq8GBwoMe0GVeHy`M92ylp_nU zA(ke!OVPK8y=LkKD1!avL1ggiOMc5r)D;LSFV0(_YVpcAHG)1 z(q$#nW*vX3Zcpi(xW`Qg2P)rE98Iiz&koH@*eWWy zb%U53UZKY}P}PiIFFUJ_A*&|-@z3=Zz2Kb0w>j?V?ZW8l>#v^Z3BmHNN=(!o;93r4 zSj*(*+1a$=p3&Un1cRR!cx{BH)geW8r4r?*)K9mjywL@`$PJb`^qb^8FE<@Jdi0A$ zjk?P~4}T8BjSZd%jwb9d@WuuZYTcjS(4G{7z9wg&cZ9iZN-Kjwj1IW4E4GxCV3^jw zgdMGJw zXVqh-Y^;)p&|hXiUSmO=`5+W zTt}}t-KC95*bzt?Xd>} zUY?(uUtFG_zL(~kNd>1&Rtv|Jz&xEL8)bF*bz*qbn1RJb<7#voXDiW#QLu$f#@2Kg zFm){4{P(&=wxPbag-=2^M>#@emERBj&K8`wgR&r9RlUp~Q|yWfBGsGFWqY1tKhPD0 z&ubjL{PxB{_}=8GpIASPuO!G+_k`|y^(yPJdhZ#+WU>rKzD(6v z;dPEzPP>e1Y#;j@FXR?WCH^yR9XsK3*?@rCB)PIo7}(oX_IxbEUu5>gPIimSgo{Te zCQB2WE*;x_4|pv~W&_i4w?$?qSXHGUf; z@RqooQ|3J*E4g1Nd+*F=!mhU;9H%Ipox&qrOAE6KohE3wo->pp+Bp>6!0^dr-{z95 zR~LggZU!aXR(-A}%K0u=Zg$}5Y5LMt^i*HF>Z0Ey+1BVpPRu79&ZJPs##`>d$E-x!DGWsii``cJqR6T33biT-gA*02dvr3C)5;Pj9B z-zL}IBC3#7KK7b8jI{?6rgz^uZj7PZmziL@6vE%EG~qA=m^9LoEp3*#M%aZJoLUl> zAc`I%z7s|lu-w;15&K@zujL~@Y}=PoiTfSbs<)wumTH`x(_>9r9YrrT6&~IcYWjqA zCWv5FCf%pYmCt!EIKu7tG9o*km*LUJV8fFFTHrr!L_hj!Zr1WQ%^(&ddNm#w_4ZxV z{E%2dWr{EXs#qE3^%EG=bkG48abL0rjhU`V-MkMB?r{(yW zz=~7l?bt?veGhil){d|Ev}Oks5Fy@h^eV|c`oooa{{SH=qM1HiKV}eU-fp$KHzCv- zC#exCr4zDr;*m8sDVBs^C2afIP%|zzotB5&;~nkk?RRI7>dMWr@oaW~PYVC2Gjp;ax#92JGmE`rL=N)|$NxYe;qo;ws zDyUaiU2hjaVD{RGOvShIKXVDVua>y9>mim}MOFCl@#S6tx!(h$IpHR~azq!td_)>| zZrKL@iLj|)2iFg>p&t9@VlhAH4fuOB-t*~&n825L{^@t07(_3(ol^p$bsoMydO9xy zumHup2mQfl;qkC59|>8TcE&Che*ebw$>XMx2MjIMbhhS6{mdbdg7tA%TdxQBSd1O| zlwd~U$<&g@qi6)?xM+ZPbTqV)bT6xwY$ZB9tEle(k7E8cq#gBsFX3vi71l|Zuo$aO zp)zSyXxwc44qHgXWXl$?&wckj<|1gpoFo#70e3fS5#F($jodkF&X^rzjf?TugpMWw zo=REUYeG~@Zbv89p+P7DZG|lM&RP-%Q;1Q~!un4Q!tMa?4l3rgcPsh%%P{v(I;}qJ z8-Z(q?LBP9d2x0ugY)Rv6F<2cZ}PX1b-iTM(nFz^-7g*p?TV&*(in;_@CV9pHD)ZO zW$MAgBeNNL{Dy&?4%iQ3L%dX$; z6%HLq4D9;Ym`|+uVe56Xiq$&3$PUy`5yghQ#u?tSd}Lg!6Ij!`yK7rV{nw|C;wLVs zpO}4@Nk_lXUVJ3_MQf`#Kax^1=nIw=KSfUIKuRraCn-8n69zVxTp`Fs$9|hj9WZb_ zg1?`M7KC3jv!h`y`ITewszOP?z+iAU3j$5{Ql6gpoRaoDs#Ox-tEqR?cx#%+CQ^1G z4S$(kOA92n54jF7dW?tVpt$9A|KN!@jXrSi>5rZE27b!$nEfg_*>z;~&K2BmaW~&y zF5J65SX7Ez@tnM&u$gdfF4EqG|}e>(;I4oo1B94F<*U3|Iav(v_pR5=vE~gwd^b zce%ruvC@zvJvucbbvy*P5BuhbAN_N>-cgROySKTmTjXRx${9^k{F5}h-AwD%olR9i z*VviD8M6)XZ&sX>x-Ys!ZarIX9e6C)B>KWaJL$szumG_avz{s!g$!duh2l={l8nbM z=8TLnK{S6*5th@cd2{0!g{Uc~31L$2cT$jDKSs0*4ZVRs&HM~)WXjD54*Mus^?UN$ z19B}sYy5nxF|o`*%u7XTjuACfzC!g2vEDdi>{!f3=ju3z;=-QAPm(T0W>7!cY^`G& ziq_(YmIl9Nr3KV{9edxVE=wct7*^MqAf&n6=!SP4#p1YdL@`uxU^XIv5k5;Mixx9q zu$sI;?OA707NG+v4rc1bN;s$-;=lZ3~sPZrIH=LL4Wz2cH=J*t9 zyz^Ibe@`MtZhK_CY)oCfZ&s1Pw&1tCZQXTmbpa<~Qgu8%_}KS;@c{W74{H)L9khB? zTfSu0TsiBdrn>S>`FD=7n*ZDup4ZJfuN@@PuCURP;OQXu(+ zU2vc9>rRoa*g>AE=zP?g%YMfFV1DOrS!!b`j8rmRT6d6?W1)hG*#^Uk zdR3CdYL_$eJai*%R^!d+=_~TdnD=>WnD4UdC5~f^u93`r@+YCAFKf1PuWC|dbk2ix z^qZxpI^cTLrqRNtpALWQ>V_RKo-QiuCmST{cc3R$y9yj~m-t=H(w|X%=nJK-W|qz8 zWxuL@d@=jl)N^=J50iGex&%$$upcL~NP_5xpG54Uc;;drd1Z&)A$fXYnquq1cN1dC zqpH9CkF=O1Ml$ZNs69F3LKXYwY(<>T$#{$-5D#JMSD9lnxW5&*&@Ow)EAD zv;XihD$u~}VwPZQs$X|?^)`q6D4grdE8IiH>nW-&iWe<)+NE@b5+U-$j)GMH1> zQjJh7`k2>qqn4psAz`jF0x<)J_CXa=X&}l00)71g~^qE~VXK6&$zBUVMcs=Yx^-)=|56 zV%jm}{i^6M@(!8pzHdDFX3?roFJ19oxAT0|s_5Va4JPtOz8wg>w-I}UBJI3hzHI3p zf%kdIk;L9{3rvEMe~wPq)D5)$o?XI`$s?MES8rfOD=YJ5WI8@22_C;uQ5Ua-Dt3#Dn6DGb+&+4+C*s7S=2De{Txp)=h zs@}V|Ix-%|+fTrY_@TiAH>T%@KActgg4Mc)*>m#+*XDS^hx_l16=pINYV$fNQjH1e zc=I}x;HZq8l+=L#QR)+Oe!(|PXcfR`q2k*Ffr@1KMr5INuSfWY=JzSFd zQpNX5x>;~j*PooK9--kqbAy6TmD}Uz3eM*!MhNjKq0!AZXw%-;WKafC%63Y4$KCtZ zywv>SD?EIBtxFWqoaJ{p;N@=u9hBo(r!7Kl9g?p!1IH&B4D!y8F&`npk zv~-bK@BwPDUD8Td=}n3~*kF|--hB7`DWL)shtS7;x<6zZg+{JJ*Wz@57_sPWpOG|~pTRSBBmMq3B&DoKY^PMd3 zL@&me4EQ@QGxq*IT3`8kcQr%SL%sr2Nnw$CW(`utHB}xGa5;|EktNkxRT$n|L#O!#3o}FMx>fkMGUWP>M#CzuCq-X6|31G?vlmG4cuK|4^<7<-(g zY}mEU=8}m#-_I*_`aBdr z)t28QG$Zo!Kv9g^$Q8PpV=hAHS2zH>1u8SA@CtzjDCCCQ@mvt9<4SwBVHE3sZlhUCXl zBEG;{Nu;b3dIKf+Zl$O~=ru2wb?67($Tt9#W* zf*Tm=YVvIvfk?e8T=TczBO~L1&s-wM)5WjL;4UGy4kbJzWG78|8%+(|r)GvIVZM2b za&CJX;+--4AGKW>)?Fm>KJ=B>EO-$e@us5np_7%KyjdBh~iJN2hkz83(---F!aX2}cpq3*O z3WA(`J<=CknlfFwC%w@$ANM#tCz9|<3V)K`#pxD=4zqR%e ztcjPiL`g3%6`LnU*M9Lmy5})A{C?p0wSKMJ1vO46%m(c6S}Wtn|6di?8q~xUg*OJG zj##aI2&hr4V2c&x6;i7p_`)J+TOk&ifQDB@Vi+QiCYVT(Rt(^y5e=h)3RuF!I!Xjn zf?x>^;$s{@VF+LvAb?eZXcm{yv(b<^vp+I-_uliJbH4MPy*s<%r)XR~5Qn1TKD{+6 zTfeN?M&%6muAlP42Bwk}qBL1h-Kjm^legbAz9{-ht#3oucCPS9WdN5vUZN_WK1D9K zZTmBQI(-3@bJL;VT4vaWn89l2izWOm{DBF>7oB(zlXN+6-!Lbz+7(aFyF?wUy8<0o z!AX9xOtj;1>{aZ*JcKcFp>>QXbZZ)_mR`E(mQ21qS039RKSRvg?)$bp?eG2D4HWk8 z+a{iK3aZ41Yl68AQM9(N?ytVe6RWCt9*A%<^W54<;a~Z7&9Ng5ly2)dnbTR$P$Jfe zmF7~*Gjb&J+*VQzrvdm@Y$dmOcpP}?LZ1&a-I{C}La!)k)L#E9+NM|NgA*l?DG5(} zI8jqTh%}xrcaY#hnhesN0O7|YEd687K9ayV`OmBaokVzu`H=H%7%2{X0{06fII8z+ zkJ;1@G5pvvYZu}zr6N(3rw-ax2X|;-*saOl9YS{oXa}>vus02zw5k6!d#}5gwY3GQ z&@sa_k2$}HdLAIJ@(vM;wPygt`%8DDuaPyzRoYL8QV90xcSlJeF$tg+UC_0p|ExOM zNEYPT9kJaq+<()LVU-(kwgsuwMU!YKRTZh}*)q{rI*_?ZtuL;z>2;2XVFR?ep@!O- zg@Kji9~wvFlT0f`BL`2%{>va04r@6J39=v;Vrl^dm68^my&m!G7M>vh z+#AYLEoqhf3G6{yVZZV&oUCiQVJ;F^Yu(*1DbZ3Qyn>`?8MAgGE*-k1v@lu_ih6># zwRe=#nF1Eg!gcuwVek}H%<{Jw^Sy^;CejN_F9&^0J)B@d0bV5N!lL9eBa6__h`A93 zW^d`5F5(8M#KO$d7^EF%Sb)W{^7|ka5619=pT=*gytX+3-l?!6Y}@3wYu+gZTjn*L z!j38|id65yl(~IieSefYael+4jnKr)@W9}ZjQMB_;3CjS(%`6bi0CH5NAX5OnAaqg z__Ao9x9dmtMOOy6cEL2HPiLgqBMAP4?T!4gi_dc@%&C(wfC*mE)>W(7{iFbTfsbnH#^dl;ZN z>VLXvG!Q5hQg^S{d8uNL1k<__TyJzZJfa?E;(^S!3)iSq<3$($BLq~D8zPOVGz}*i zht>Ek9L~NLMCF7tj($vtH=!qVN8!)mt25OL2h0FNQR&WbN1?r<0NRQvh@t3HvOpEH zBoWh@J#TmR?E)drQFoZ9OGDAeHX8y$XN{UD5aQcv3h%m}jH$B(eTr>~m499Km%aLN zbF^|o3<w)V zp=xKoinz1Jr%?O(bcIVp&@r+=IZm)FkP2l)3}TcHTSkYpt2aMBBS&E@RiKQtF&d$v z$E)rmymjqIv)@@Z3j+802q7SyM&I$C26<{gRTW{OBO1I$VSmdhfK4X_WGP{2=(5RP zd~+EWnV9O+Yo=+9%Z%it`2yB)api2va3&j5%FffW@%6jXim8Tcy4Go}n(KjZX*M4u zn=3jvt)}Jp!;p%>F=&J!!ypTjI;A(U-stk~jnxLn-ls;0ATvx*+tv`XmR{fE_bhmY zvGCuq1+Pusax=W46vB5^wrs2x!(xBK!9mRAw=A%A&%j05mQik(L@=3r&4>3+rlyvZ hEtkU&5-tWr=22" + } + }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/sqlite-vec": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", + "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", + "license": "MIT OR Apache", + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.9", + "sqlite-vec-darwin-x64": "0.1.9", + "sqlite-vec-linux-arm64": "0.1.9", + "sqlite-vec-linux-x64": "0.1.9", + "sqlite-vec-windows-x64": "0.1.9" + } + }, + "node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", + "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", + "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", + "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-linux-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", + "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", + "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + } + } +} diff --git a/apps/desktop/native/package.json b/apps/desktop/native/package.json new file mode 100644 index 0000000..b4b744a --- /dev/null +++ b/apps/desktop/native/package.json @@ -0,0 +1,9 @@ +{ + "name": "quirenote-desktop-natives", + "private": true, + "dependencies": { + "better-sqlite3": "13.0.3", + "sqlite-vec": "0.1.9", + "sqlite-vec-darwin-arm64": "0.1.9" + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..8b2ced5 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,21 @@ +{ + "name": "@quirenote/desktop", + "version": "0.0.0", + "private": true, + "description": "QuireNote desktop app (macOS): Electron shell running the embedded server and web build", + "main": "src/main.mjs", + "scripts": { + "bundle": "node scripts/bundle.mjs", + "natives": "node scripts/natives.mjs", + "setup": "pnpm natives && pnpm bundle", + "dev": "pnpm setup && electron .", + "pack": "pnpm setup && electron-builder --dir", + "dist": "pnpm setup && electron-builder --mac zip" + }, + "devDependencies": { + "@electron/rebuild": "^4.0.1", + "electron": "^44.9.0", + "electron-builder": "^26.15.3", + "esbuild": "^0.25.10" + } +} diff --git a/apps/desktop/scripts/bundle.mjs b/apps/desktop/scripts/bundle.mjs new file mode 100644 index 0000000..6af979f --- /dev/null +++ b/apps/desktop/scripts/bundle.mjs @@ -0,0 +1,53 @@ +/** + * 打包内嵌资源:esbuild 把服务端(workspace TS 源码)打成单文件, + * 复制 Web 构建产物与原生依赖到 dist/。结构: + * dist/server.mjs 服务端单文件(better-sqlite3 的 JS 也打进 bundle; + * .node / .dylib 由外壳通过 QUIREENOTE_* env 指定路径) + * dist/bin/* Electron ABI 的原生二进制(由 scripts/natives.mjs 生成) + * dist/web/* apps/web 构建产物 + */ +import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { build } from "esbuild"; + +const desktopDir = resolve(dirname(new URL(import.meta.url).pathname), ".."); +const repoRoot = resolve(desktopDir, "../.."); +const outDir = join(desktopDir, "dist"); +const nativesBin = join(desktopDir, "native/bin"); +const webDist = join(repoRoot, "apps/web/dist"); + +if (!existsSync(join(webDist, "index.html"))) { + console.error("[bundle] 缺少 apps/web/dist,先运行 pnpm --filter @quirenote/web build"); + process.exit(1); +} +if (!existsSync(join(nativesBin, "better_sqlite3.node"))) { + console.error("[bundle] 缺少 native/bin,先运行 pnpm --filter @quirenote/desktop natives"); + process.exit(1); +} + +rmSync(outDir, { recursive: true, force: true }); +mkdirSync(outDir, { recursive: true }); + +await build({ + entryPoints: [join(desktopDir, "src/server-entry.mjs")], + bundle: true, + format: "esm", + platform: "node", + target: "node22", + outfile: join(outDir, "server.mjs"), + // better-sqlite3 / sqlite-vec 的 JS 一并打包;.node / .dylib 二进制不进 bundle, + // 运行时由 QUIREENOTE_SQLITE3_BINDING / QUIREENOTE_SQLITE_VEC_EXT 指定。 + external: ["*.node", "*.dylib"], + // 服务端的依赖(hono、better-sqlite3 等)在 apps/server/node_modules 下解析 + nodePaths: [join(repoRoot, "apps/server/node_modules"), join(repoRoot, "node_modules")], + banner: { + // CJS 依赖里的 require()(含 node builtin)在 ESM 产物中需要真实 require; + // 用别名避开 esbuild 在打包产物里自动注入的同名 import。 + js: 'import { createRequire as __cr } from "node:module";\nconst require = __cr(import.meta.url);', + }, +}); + +cpSync(webDist, join(outDir, "web"), { recursive: true }); +cpSync(nativesBin, join(outDir, "bin"), { recursive: true }); + +console.log(`[bundle] server + web + natives → ${outDir}`); diff --git a/apps/desktop/scripts/natives.mjs b/apps/desktop/scripts/natives.mjs new file mode 100644 index 0000000..cef11aa --- /dev/null +++ b/apps/desktop/scripts/natives.mjs @@ -0,0 +1,45 @@ +/** + * 准备 Electron ABI 的原生模块:在 apps/desktop/native/ 下独立安装 + * better-sqlite3 / sqlite-vec,并用 @electron/rebuild 针对 Electron 重编译。 + * 独立目录是为了不污染 workspace 里按系统 Node ABI 构建的同名包。 + */ +import { execSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { rebuild } from "@electron/rebuild"; + +const desktopDir = resolve(dirname(new URL(import.meta.url).pathname), ".."); +const nativeDir = join(desktopDir, "native"); +const require = createRequire(import.meta.url); + +const electronVersion = JSON.parse( + readFileSync(require.resolve("electron/package.json", { paths: [desktopDir] }), "utf8"), +).version; + +if (!existsSync(join(nativeDir, "node_modules/better-sqlite3"))) { + console.log(`[natives] 安装原生依赖(better-sqlite3 / sqlite-vec)…`); + execSync("npm install --omit=dev --no-audit --no-fund", { cwd: nativeDir, stdio: "inherit" }); +} + +console.log(`[natives] 针对 Electron ${electronVersion} 重编译 better-sqlite3…`); +await rebuild({ + buildPath: nativeDir, + electronVersion, + arch: process.arch, + force: true, + onlyModules: ["better-sqlite3"], +}); + +// 只导出运行时真正需要的两个二进制,打包时随 dist/bin 进 .app(不进 node_modules) +const binDir = join(nativeDir, "bin"); +mkdirSync(binDir, { recursive: true }); +copyFileSync( + join(nativeDir, `node_modules/better-sqlite3/prebuilds/${process.platform}-${process.arch}.node`), + join(binDir, "better_sqlite3.node"), +); +copyFileSync( + join(nativeDir, `node_modules/sqlite-vec-${process.platform}-${process.arch}/vec0.dylib`), + join(binDir, "vec0.dylib"), +); +console.log(`[natives] 完成,二进制在 ${binDir}`); diff --git a/apps/desktop/src/main.mjs b/apps/desktop/src/main.mjs new file mode 100644 index 0000000..c9b5929 --- /dev/null +++ b/apps/desktop/src/main.mjs @@ -0,0 +1,102 @@ +/** + * QuireNote 桌面端(macOS)主进程:内嵌服务端 + 内置 Web 界面。 + * 数据保存在 userData/data,与自托管部署同构但开箱即用。 + */ +import { createServer } from "node:net"; +import { dirname, join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { app, BrowserWindow, dialog, shell } from "electron"; + +app.setName("QuireNote"); + +const here = dirname(new URL(import.meta.url).pathname); +// 打包后资源在 resources/app/server/(files 配置),开发态在 apps/desktop/dist/ +const resourceBase = app.isPackaged ? join(process.resourcesPath, "app", "server") : join(here, "..", "dist"); +const serverBundle = join(resourceBase, "server.mjs"); +const webDist = join(resourceBase, "web"); +const binDir = join(resourceBase, "bin"); + +let closing = false; +let closeServer = async () => {}; + +function freePort() { + return new Promise((resolve, reject) => { + const s = createServer(); + s.once("error", reject); + s.listen(0, "127.0.0.1", () => { + const { port } = s.address(); + s.close(() => resolve(port)); + }); + }); +} + +async function startServer() { + const port = await freePort(); + process.env.PORT = String(port); + process.env.QUIRENOTE_DATA_DIR = join(app.getPath("userData"), "data"); + process.env.QUIRENOTE_WEB_DIST = webDist; + process.env.QUIRENOTE_SQLITE3_BINDING = join(binDir, "better_sqlite3.node"); + process.env.QUIRENOTE_SQLITE_VEC_EXT = join(binDir, "vec0.dylib"); + // 桌面端默认关闭种子数据可通过 QUIREENOTE_SEED=0;保持默认与 Docker 一致。 + const mod = await import(pathToFileURL(serverBundle).href); + closeServer = mod.close; + return port; +} + +async function waitForHealth(port, timeoutMs = 30000) { + const url = `http://127.0.0.1:${port}/api/health`; + const deadline = Date.now() + timeoutMs; + let lastErr; + while (Date.now() < deadline) { + try { + const res = await fetch(url); + if (res.ok) return; + lastErr = new Error(`health ${res.status}`); + } catch (e) { + lastErr = e; + } + await new Promise((r) => setTimeout(r, 200)); + } + throw lastErr ?? new Error("server did not become healthy"); +} + +async function createMainWindow(port) { + const win = new BrowserWindow({ + width: 1440, + height: 960, + minWidth: 960, + minHeight: 600, + title: "QuireNote", + backgroundColor: "#141311", + webPreferences: { contextIsolation: true, nodeIntegration: false }, + }); + // 外链交给系统浏览器,应用内只导航本机地址 + win.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: "deny" }; + }); + await win.loadURL(`http://127.0.0.1:${port}`); +} + +app.whenReady().then(async () => { + try { + const port = await startServer(); + await waitForHealth(port); + await createMainWindow(port); + } catch (err) { + console.error(err); + dialog.showErrorBox("QuireNote 启动失败", String(err?.stack ?? err)); + app.exit(1); + } +}); + +app.on("window-all-closed", () => app.quit()); + +app.on("before-quit", (e) => { + if (closing) return; + e.preventDefault(); + closing = true; + closeServer() + .catch(() => {}) + .finally(() => app.quit()); +}); diff --git a/apps/desktop/src/server-entry.mjs b/apps/desktop/src/server-entry.mjs new file mode 100644 index 0000000..6949dda --- /dev/null +++ b/apps/desktop/src/server-entry.mjs @@ -0,0 +1,26 @@ +/** + * 桌面端内嵌服务端入口:与 apps/server/src/main.ts 等价,但把 HTTP 句柄暴露给 + * Electron 主进程,退出时优雅关闭。打包时由 scripts/bundle.mjs 用 esbuild 打成单文件。 + */ +import { serve } from "@hono/node-server"; +import { boot } from "../../server/src/boot"; +import { loadConfig, VERSION } from "../../server/src/config"; +import { bootLang, setDefaultLang } from "../../server/src/lang"; + +setDefaultLang(bootLang(process.env)); +const config = loadConfig(); +const log = (msg) => console.log(msg); + +const server = await boot(config, { env: process.env, log }); + +// 桌面版只监听回环地址:数据属于本机,不暴露到局域网。 +const http = serve({ fetch: server.app.fetch, port: config.port, hostname: "127.0.0.1" }, (info) => { + log( + `[quirenote] desktop server ${VERSION} listening on http://127.0.0.1:${info.port} (data: ${config.dataDir})`, + ); +}); + +export async function close() { + http.close(); + await server.close(); +} diff --git a/apps/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..5ad0dc2 --- /dev/null +++ b/apps/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "77e3744b0f953e10a9d022c521f61508ebcec1cd5ea02d25065dd0c1a0db9046", + "pins" : [ + { + "identity" : "capacitor-swift-pm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ionic-team/capacitor-swift-pm.git", + "state" : { + "revision" : "4c7f346d16196e21fbe23d4a7a6fc7af62af6742", + "version" : "8.5.3" + } + } + ], + "version" : 3 +} diff --git a/apps/mobile/ios/App/App/Info.plist b/apps/mobile/ios/App/App/Info.plist index 2ef3acd..caafee7 100644 --- a/apps/mobile/ios/App/App/Info.plist +++ b/apps/mobile/ios/App/App/Info.plist @@ -77,5 +77,10 @@ UIViewControllerBasedStatusBarAppearance + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/apps/server/src/store/sqlite.ts b/apps/server/src/store/sqlite.ts index 34b36e3..1efe040 100644 --- a/apps/server/src/store/sqlite.ts +++ b/apps/server/src/store/sqlite.ts @@ -173,13 +173,28 @@ function migrate(db: DB): void { } } +/** + * 桌面端打包时原生二进制没有 node_modules 可解析,由外壳通过 + * QUIREENOTE_SQLITE3_BINDING / QUIREENOTE_SQLITE_VEC_EXT 直接给出文件路径。 + */ +function openDatabase(path: string | Buffer): DB { + const binding = process.env.QUIRENOTE_SQLITE3_BINDING?.trim(); + return binding ? new Database(path, { nativeBinding: binding }) : new Database(path); +} + +function loadVec(db: DB): void { + const ext = process.env.QUIRENOTE_SQLITE_VEC_EXT?.trim(); + if (ext) db.loadExtension(ext); + else sqliteVec.load(db); +} + function openDb(path: string): DB { if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true }); - const db = new Database(path); + const db = openDatabase(path); db.pragma("journal_mode = WAL"); db.pragma("busy_timeout = 5000"); db.pragma("synchronous = NORMAL"); - sqliteVec.load(db); + loadVec(db); migrate(db); return db; } @@ -884,7 +899,7 @@ export class SqliteStore implements Store { async restore(bytes: Uint8Array): Promise { // 先校验快照可以打开,并且是 QuireNote 的库。 - const probe = new Database(inMemoryImage(bytes)); + const probe = openDatabase(inMemoryImage(bytes)); try { probe.prepare("SELECT COUNT(*) FROM notebooks").get(); } catch { @@ -895,8 +910,8 @@ export class SqliteStore implements Store { this.db.close(); this.vecTables.clear(); if (this.path === ":memory:") { - this.db = new Database(inMemoryImage(bytes)); - sqliteVec.load(this.db); + this.db = openDatabase(inMemoryImage(bytes)); + loadVec(this.db); migrate(this.db); return; } diff --git a/apps/web/src/components/AppSidebar.tsx b/apps/web/src/components/AppSidebar.tsx index ef312ec..f5d6efc 100644 --- a/apps/web/src/components/AppSidebar.tsx +++ b/apps/web/src/components/AppSidebar.tsx @@ -1,5 +1,5 @@ import type { Notebook } from "@quirenote/core"; -import { cn, Kbd, Tooltip } from "@quirenote/ui"; +import { cn, Shortcut, Tooltip } from "@quirenote/ui"; import { useQuery } from "@tanstack/react-query"; import { Link, useNavigate, useRouterState } from "@tanstack/react-router"; import { @@ -29,6 +29,7 @@ import { type ReactNode, useEffect } from "react"; import { currentLang, setLang, useTranslation } from "@/i18n"; import { api } from "@/lib/api"; import { contextMenu } from "@/lib/contextMenu"; +import { SHORTCUTS } from "@/lib/hotkeys"; import { qk } from "@/lib/query"; import { resolvedMode, useTheme } from "@/stores/theme"; import { useUi } from "@/stores/ui"; @@ -260,10 +261,7 @@ export function AppSidebar({ > {t("nav.searchCommand")} - - Ctrl - K - + )} diff --git a/apps/web/src/features/editor/embeds.tsx b/apps/web/src/features/editor/embeds.tsx index 842050d..e5bd143 100644 --- a/apps/web/src/features/editor/embeds.tsx +++ b/apps/web/src/features/editor/embeds.tsx @@ -16,6 +16,7 @@ import { useCallback, useMemo, useState } from "react"; import { autoHeight, useCardThemeVars } from "@/features/render/RenderedSegments"; import i18n, { useTranslation } from "@/i18n"; import { api, apiBase } from "@/lib/api"; +import { modKey } from "@/lib/hotkeys"; import { videoEmbedUrl } from "@/lib/video"; /** asset://id → 服务端地址;其他地址原样。 */ @@ -194,7 +195,7 @@ function HtmlBlockView({ node, updateAttributes, selected, editor }: NodeViewPro
- {t("editor.htmlEditing")} + {t("editor.htmlEditing", { mod: modKey })}