From 8cc9fe7ce59e236d3f1d4e62d17f09a8621264d8 Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 10 Apr 2026 15:36:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E7=BB=86=E8=8A=82?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 6 +- assets/images/card_texture_glow_lines.png | Bin 0 -> 64699 bytes assets/skin_config.json | 2 + desgin/funymee_home.pen | 2 +- ios/Runner/Info.plist | 2 + lib/core/auth/auth_service.dart | 33 ++ lib/design/pencil_theme.dart | 10 + .../generate/generate_progress_screen.dart | 98 ++-- .../generate/generate_result_screen.dart | 546 +++++++++++++++--- lib/features/generate/generate_screen.dart | 222 ++++++- lib/features/history/credit_record_tab.dart | 151 +++-- lib/features/history/history_screen.dart | 90 ++- .../history/widgets/history_grid_card.dart | 160 ++++- lib/features/home/home_screen.dart | 61 +- lib/features/profile/profile_screen.dart | 28 +- lib/features/purchase/purchase_screen.dart | 12 +- lib/features/shell/main_screen.dart | 48 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 8 + pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 22 files changed, 1221 insertions(+), 265 deletions(-) create mode 100644 assets/images/card_texture_glow_lines.png diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1456c3b..863c70f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ + + android:icon="@mipmap/ic_launcher" + android:requestLegacyExternalStorage="true"> 0SRg820;*zE(t-pK|&Z>loF6eBo&eFQa~vw zsqYNl-}8L;k1LMD`<~i+uf5hfktoeOq(lrv5CoClRaMf0Ae;~g!jgmGgHM)=-&R4; zY0zCId2O%B&1UaR?a{Q0i%j=110OSj`?q_=5_Iy}1k(B+ci`O*tg6407-2vC@LPn6 zfXcKB&V6|b+ExVqx7^jkfoT11>8J^|s27_WqBHMjd^Tp?pZj}dfBvba zr&mjseRSM4%9g$N|G4keZqK2$OSFVhYm?c7@%v35(|i7`JiE`6Px6fS=Rj9EO>U7K z3iI3A`?si)gV5%6hxps*3HDz<#giHbOYE~T4<@Gjz9+8z8SdvD8@%Xi#Pe7O$L_s0 zF*OXWRftSNt3d5`eCMOwB2;kjfn9NsS#x6j`CocPU7j-KKDF{G#S`oZ%qR+xI)xt^ zR-n>vextm|zmx5gw4byupCEa7@V=|&{-kPnh=JZc9n|~q%SUtKh3w-fc#vYgWOx1Q zxX4%eN}BJ6)SFxZoN&yCoI3lBv-_UQd6V`i9+t{UWgYOA)~e@@1szBpCYVkY&F=yt zT+D|^o$HX9^t9&aX)c%&6&;xa<4?KLP(@?vGG(7)%mS2X>6C7mysz;REZfK5{Yw7)Q&VAoRR=~xhhrV%+MF)P=WA7w^gE;s&lxGu59=%WtpbIcPH4Pc~st8GweD5<2P zo|+C^`@g3$6cDj~W4~8bH>81;hj|Yp$DgQl0!4iuhsQdoK+NxIm#mYsmwjh1nmlfv zm8qpfXph|mpOW6{oo$miG{NaJgT>crg2hUj4#e z^)JdQ9|zKk!Pbl-usc+AhzZ_>zKhq9zZ`KPT7LDxK_l$&&xPB|QmeM%DZHSd%YWKC zjME{`vqI^S>r_o{m1_U{zA&9ItIZZMU`!j96+y-sQWm68G65DG0zeiptIp{_dz3EA^zgfO)e;I*D9ivf&Nt~wr^%kNnzBi@^ zWJDWZuA%>1sltG&L%H#U$u|L)Q~CSLcg}vB%ftUBYCOSU-0-GL<;lkAHKJw*Wu40n zL*p+Ex(ee9jLKBJs{NWxrHprZ;zmW;H;Jc7<%n%9QDSjCuP@gMnS|XwJA@9QC0sc- z>3yyEFR>W&I1JN#T@NGDiTW?lMnw^ur1ksnT8o$MKhK!__Q9NjB((g90`ggdw;sL! zXrr<=eb2v77rx7AuPapk8*Z2hv8{7%bvuog)<{Ep%PL$qU+7<|&{1@JTCep3jr$pG zi&g%|nv?D_wRD5S(#`W)I?_ctz4w~}k2e3my`X|0%!uW_O1}2rJ~SDs=F#p@T27RG zkIh3XYhMnXlmJmyaPytPNB}ah>Z@|Ze}^TLr@$|htL*fkWssOt=U@8dKyb?rJ>-Dv z?uq^P#NuxXq<{S@s@A4PoBqYFmX0i+cK{%%s?Qb$ z4U{g=z^F?WYngV|hW&SK=KxgO7GmE#h|F;LUhv)!Q>F8qa_!t-B za;fC!6ebyK=)W4Ke39w8$9}nf;31p@6?Rst&w`qeEQ~Gp{u_)2`=;6-+}Uu+6ge0N zn&xvI?TLSN<$oted-SQiJdBXj=<&ZD>}FR`McL)Y!rPI7EVBE|c>BL7tSg&zxE)MbPeuM*ihQ?<3QhyHI{O3;Dsk4i2ceibpe zhtS?i#>|wof#&O9xx9ZlLaPuhA~!zeEm!Q)T?NwV`o9&t$6m%(ke!@CuD$m!wvnT) zJh-{!dmQUYmqVvhcMnybQu0aVS}wFA>hiO+Ci*aAa(BGxaxKAA534DXihWR)@03b% zdd0_9KK|RT`6!_vhWhA(PS;HL+$8qk|D_o8-yWWKDaxHJ(`WnCB$#Cp+t=mb z&Z9{VZFkCillgDQlhBjwA;W#%t%5~D&&Y?AD_1=)17FxHshychUaMTIIHiwO`!i-D z_&z@y$r>wyi)`g!Slx6-E#%p2CA4>RK63htO+75UruuiAiO4ZO1pYBQhYu-hP=_lI zRcxSf8nOgwfW6-h_H+}ZD^o`PvNczAdU3JXJpgoWj$n8WqB z@r!KHv}!sZuO?#;2H9!1sGRxQrsF4D#y_3&s}oOCf2GH*;xMI~!+}{D^q#tzc*^~s zGR2RJ;?R18;Ip=29ao|Yna!$>=@gUa6X!qg`P+$thuhn8**ZC~^**LFuclMFMzJ4M zg4WNl;Yg~YOsZf|Ox5~qaWn+@b5hFQ;_q)#bvG5q;4v z`t-=G;o!^bvRiYSRG714SDfNFWjZASW)Pv0e}ghIz*4`;X{HedwI*^raBFx(_I84M z$f^d6X4y@AzrAGLH4No+M$(}RLIv9r7P!L9H)p(QouqZco-nk?Rsl?LU|^u2q$Kk6 zs7KaGW;NA90{rL2nY37?;`-FIUmptAtG-L@x{ z-M4~Zh4&PzAUO>=e{adCPT_-=uC#xDHk2c^jkC9GLca zm6;+gG@oh_+q>c>{%OT++Lg(Li9lYf*$a*2^ph^}k}e6C=I~EKzmzTs-bWE>jD*+- z5%oTP)xG5EwGc{c5G(WdN31#l*zEs;8K}wR=>Ex`bw7Jap+rgtd-}LJT** z*ibo4h3#*Q-6_({O7C0S3veueh1uONMbFCVZ625hjH$}OpwOZF*WwZ2yT;Fc7AD_K zVDorfqyOo#AQ2HJF`xtU{(G>;f|QvM15ZD#6IdW@h~IJYUFMQlK$Jm`t2Sc^x*1XV zDznu*Zp;wDeRzgWdr&4!wHWQhO>_N=O=8pOZr?xxo5p0qfK@}#Kez&C=l)53eePck z&;8c!j}A2#Ag*=ns2P%B#Dh`=Es-MADrkjY)bQ^Nuu{kC<#_M|9IDq^#krT|ih+vg zU;RmXm)bR_wLJut;_t=^Y(=~f>Gp>u2LpCiL>;cwD{cfnc4k-E z*`-dmJ0!tj>q%4Sas3upfB`o#*rDr%mk{rVRh$`Jw{g*JMp36|Y)CyvMmmbWxD|zE zcCY5yN^bQl`p#L%%z2@L8O3fz)Jx3QG%Vr0yL|9OV&Fq#qn=2Hj*156=#3&!#s7+a z?yksJ>uhf}B8cC8Rh^!s&BLgo)#a$wRC-d1>v$sRG4TQ3rX67@*ar~dci9qdFuzP# z%l4_*FV2s(o2#)VS1Z(#%F-;wxrAX15Ru3JD@;!?I=j-yTVFxkvK72_GVNgm;%a%^ z;^lxrvhw!|BSuC3ca9m7v5IgK52J^s#YF3g(!z*|u;vm65#mIxiVca>qPq|g1$F14 zpJ{YS^ySv9&t)o5OXgu-G~6Si)%TXjE)P=F7g#pyLRr$a$LYYhbT)HSR0L6GYBoHR z%6XccX6*TKP226n<#$;8p7^jOEO6#m3H0$tx0~P3`k<|I;{7I(CXv0HZh6mpznx@L zxU~3MvH2|wHYNJYY&Um>?gfdW3sX3GGKr#ss@Y+jd&TTFK3==qP`)nz!^-G(kTyje zC%--Gq`31WSiw^8_BOG%#)#aQ$FRp$80!rfcJ|O3)Ucb9uVB=z>UD(0HenbP44 zkl?6~+z2y+)|jM*f*`;>R437HaR`5t}${Wdu>PB z=YW4uoH$4h%Z!?hPg-8Ty8Z={y6Zgbfr4sFGH0G(8vS;cR6NZdURR%x#_J7(T2{PkN z!5Q;tu+$IWxpP(?H$PZzMh6Ec0;GX$KCkLjA=|HWU^7`m;K-671snCQ%2`g!@;vwy zoU~&;;nuy`vvy*>XtBRP@}H^^-?0>YKmb=t;-ZYqy;e_)JD4f%UiZUzzCO2SZ_>r? z@Jc0xQvQ9)7!@!QRw?j}ZFM6S%=Nu0lRvJCPJa;jD% z+k2Hh8vRM>T73JG(HIZOoT!{62Nv``Yl;}PF_b)Dw+m-1ks885a-fg^DY4zlZt@u^ z(zqTgwiq!}tmHbF)T{2OofY)zN{q@%*Qqf)VvTFeLp=MXQUxaqZy4e&X9T~^lt`J$ z$LF2jN*+92+dFPox8zM(id`aUu*uVM%saYxZ$DNGLH(%$`8Zhctao6NLu-4X8!QaJ z*%i~;utL!#50g0Q#^pVpEGzT~o*GhjEsy!JNg-rM*FO4)QyJw!aELyOLa)y-pbZ7=c_<$>s_xMnF50M$Nggl-pY?uT-r8~(Ms1IcZ88%hoD&T)l{%2 z6;PnfjoXRFIh2lhAmE3ENOdcXXF#=dU+pKGZtC1f4rmFY{6zO4XGY#eFdx(gG#2}l zVx^}@jS*FuoSrQGRwI$_)S!;MG&5E1z95f{?_Dl$p8x&xliu6)y9pYEyZQ*XE$5PV zwjFIb1mkig=E3twLi@oi&BPQDOb#fz7!_8UsQAZ%cNcfWyni>WsW5|}cl0^fGy{m? z&4{f>>%N%`O3Onfvg?%BRzS9lwJOn89UQJOv(~@+8+SCbB;IQ*e>N8)a@KzHgP|^T zalhmy3>!?P=w8ljU&NOR7jHFtrbQeL_j2g0lh0=z#cUF)|aJ?OW@v3_mnBz zl#9?JddaC0-Mn^?%d&QaPTSvjnRDMdfgS`v$<~Q_TK}KtGN@t+JopnM+C5amx6sz` z4fB>%+i1tB?ai8@SlmnCz*rk@RihufwwSjm7{#V&KUU!g5QTt+icfJc-qd67>vXS6 z(2g6$S|H0ZWO4`!{_t ziI>^$&lg8Gn`UsZ>||UDzIhCXt&v~}pu$DyVgHqUI@~PWaNJw>B5}bBE!= zuxrr54x3!MYLNg)Em*bTuOFkB3d4Ns1bjzylSBvj|dys{I5h-rQE(L6^hw?-srI{Mh3zP5duV9lr_=@*o zj?{*VBv~Z!r&_jY;q8Dfxw5ZQ_<6nk#k~o)wkX+dGfCcD&wC+imz+rwSo~Z2HVb=+ zgm1kwQ?GkaF_9BM+f>lDMXY!c56FNvk;{t=eFP)NLS@)QaBc_0{2V3j>pllb^m8UU`_Wd#!>^uALwU1PY z_S}c0uAfZsI|BK0@5_x#BFb>HIbDg$@z3$EQz6-IYYV1Lytf`==2f@yBE9Y(5%CcL zLD2%d61-md3W%^zrN^!7FH2Q#C;-qG*=1f#^xBprMcsmE?LOVI07a24li;!=HMeR3_P%lz^2?h3`AI6yA~_=#F#d;PcFiy4rT z88eW4+SgXoI;^?AEUGq_gUyMOYt_`~fj5BxRa!RdgfO+4xd%}{O@rILq%#MS1WyAq^sE-kaH-DLnF2Xx0YKo|tw2tM5FoFpHjk z6gzsTSPBPXa_<-TP$f-e^0Dn3^9uy~9ZLks3kyqJhM@aM`Og`f_<=w8ODky67>;k$ z)Kry&r#Q@}9@I{v;O+K!@?>5k28v&rA?{?>SsuDHql!)Sp&GW@iCy!Kw|FQFI=oI+JN?4UVT+24)Lfvst`t$(-geMlz{K}TDFMk$6-G*;qw{nDh=5aT;^ZYFJn zk@DAlQ(R`Q8mocH7>gU`ol5e#fX@d^4c;i+x{rT^>?uCz&n)|rmFX;SM->%dna}_J z)W=o$qQyCsjLCn&W}nQNNo`(1P>uD_@AfFEb3&YJwbY;kQ)gp>nP^s*;^VK3M`+E} z@KuSY=AuN>A_0CcOfhB?sx>joO=BTwoKZ3&tH@A zi_>#qYtJ(Sm9{&!z3>7rcSHD-j$#u+^>%owtWG`b^|Pb-U~-k457N17sR2ILN$iRf z%57_)(5rJYqO?%g6k779OanS?@(|0wdswXBzCFgjy=eUWcqLsf z&x{WAG4=psRX;!9%VBAw8^MCu3(v<|4&G}>dTzRm{Com>aBo?~c^mB$>%q*18aiCE zL-=vFv2_Rkvbu^;EIldY0#Myn;>!#bUA&H-Xa{)k*mE-x5lYB(wLi^qAr*!zH*rty zD&N0(;3g?*lUou9Xp^r5?j~_PR4HVSasVVoiB3_~50qwM@MHk~)0hw9@QOzbfe&dyx|p9G73Oarxo za5SE1^eBBm9Ey@h{PFQA{*lTd7QqlE|Gm;g)^7b<$%jrAb^zudg$$5$-qCOBwoeCf zdH!eQ9<3azOQ~23gD}z9OpDWbLO!YhXOuYN>z3(Nr`Sc{3u#4TnZhfv73sdASqIJD z*VftrisR3q;rv2EUcZPb5>-@BiOP#}zr2s_WSNZ{H-mp>Trx(dMSY7ta~B^vt^54XKP z?e-MtFN_qrs0mF67})f@QH{e6Vs;r^v$nuU`(~DLZVFb|h5*f_gwcCKKv^CFp%D*W99`fN)}Z zNB!WX-yEM@KHY-7=*+uXah{e5(r+Hl{#P&YX3N#L&A}@(~wc$OQHVp|J zH+mQ_Zn`DP`^dwHAmC*pTmgnc5;NkyrHFCi=)yxjEHSd%X(1<;D-#Ct!G56?2>~HI zJp^9)y*%iBh=TWyCdZ80nc{ed$Os&YB@Fck8~$84hF%)9GFjuy62lGPYjPg_|3yZi z3uhOxZhfIpEKgykyBIjOx6MIQi^P8ixJIAabmg#wL z=0PlbQW0M052)Uv;&WztWTohq2 zJT@d025^?7a;p?0Y=jOka77fcqU$gyjw_!_W^*ajFc9$+#;f95ivk$<)?wl;okjHj zg90PjYt*^?DSajE#B@!(g<7y(V|~5XMiqB!6<9t8=n=woU*qdyo2<0l<5+R2S2P1` zV6|Sf%*E-R1%_+9`twOTXsLgTk@f5)6hNNAaw9_W+_OC)v1?hG?n3DtgaPA%l~y0t ztr!#?FX_gYX%ZgmvA#0o*VL*^G%U~|at;JnMd0jKPQ*oOiCXRAJsOl1GXJDpVI>3P zmpm%qDbOmieO0KI(rvkq0rHv+6;ZTKs&7UfDeIFsi77p}_6Sam)wzn2oXO6^j!3Ha zIk*LQ!TG;9unoLWFy783S5>wIAwaQ%?lJaq1b~fmYhc2aZxzE4`ouRr7{rdF@xs;SU)F&u8$WjTy(E*!Yy z7n8?67HhzVD5EXg?-^?IUIT5$vwpgWWH`3Ipoh_DclgRw$_Ho8F&G&!v8N{TrBY*?i;a}uEI`yg#Eys- zn9ro0-dx9e5rZG1h)2ZJx9hUQv39Z0fSX5t8(Ntvlzseb@_oMa(De|wX>>f`zFgke zrn~g<)I~S^Zrtz$`pr0?Teg1uFa-l98D-NZ(rJ@R>qb%B#M&6EWRV1!|JS}?OuQ%~ z{8@-CjfCxkCx$dNh2vxfBM!YS0mkK{a^oMaCKhOM*x1`&2hX^X&#R&2@#O|aHt+64Rv)TP~FCpbiE z?|rn83rP=+s{!0v4Bfj}{{AFHh65j4NQ3J%#ua{a?l$OJeGk-sa_K+AbVv@_p9Kk? zpwkZb@bK6HsjFJUNav%<6xHn*dSb|>U!cdKvd}i#ce*!&bXOezrRa!|*-WE{vb7pl zsN7(K5z;6~;+?xfjJBLUY3JB=z$V(MDZe4tuXUKXRj}5UQ}uajCRjpXp_|2Ai(`5f}~rPX2RPdw+&vNu}4#CBta zSaHc@j!Y{R~FEM6}r_&cq_9CXUe)n?_)2CER}tQtRHz7(~8x~-)bF&V(7tP({>OdY`inY2y8R#W7=&C6|6Xj~V3O6t`Jj-k9 zJDT;`YWuL&qE_cLgVZXaYWP(=K}|hxkOf&gNPc8bzV$!K`c$y zzvWh7M+D{kFmG@$$+N_{actIZeo(YY1B^O7yw01@2{q_Z?ZOCJCN+L5{6@R8uOhnZ1z)4SBj z>!x7Zy{l_j01KH-)jBc*-jey{o*g0%zDRX^Iwj3>w1Mki@oq zOY5Vh7dkS|V62CgUNQ1;DxTbN&lsmB9V}BGQTp(=0_rw?U0qv-DYW_z-*5xxpVaB7 z!}Vgi(|s!fwQNx(VRnWX1)xeE&iVU)u+)ERrwT!<0~upCdDY=g9PKpihqFHS?uc!+ z#TDiCme4(p&^36adIJPD4Ntu4@B>Zpn^trx{GOZR^J*r4wOm@U2&jY?ZT~ccegCdo zNtuuiSOf!Dg#X`>94(Murd>Knor>RAQPcYER`b56{Y{3Gj~jz)*u@_l>rQq7G;13* zHDa^qz^xNptYwVOq6**_>vq{2#hZkDA)LZCCAHLPT0|_q+gOgE(K4IoBJkNgEGd|A zpL}PV0k|6Tjqire9zHgBt8djRmc#jBu5okaLe27({&^2=vMV z+}Txew*rCZr(PBV>B2d9kRSl}bHG<1Joo*3@*VpvyVVoZxOZ99I7&ui7@lPfu^c!*bGx}I=<#VS~$>r?UYkrltrXZHo@Vv`^z@* za6kikziQVDNWrX4H)}Z4D?X96wWX*KQy>^CaOGjtR#&S1qc%U}i7d=eGcex{a{Ci-Mj5!na_mfaw9p%?9`yF#Jt@ zeyoJu6i7~bz|a7YL}@AVtbIk)Xdiidy6zvDhAp7{CG248WoOh5E_oh8;ZVk)A(h(^Q-&_(33 zr`uTO`AJb_^B+O&PVQSdYFNv*NA3ko3u)Nc*_oR&!vRjB&G7n-cqB?H>Jd+MpZ0Ro z_(MLv5Dl;s<{M{h?WAc&z*;?wy!o#J4gl2Q$AELp0uT$lp)Z+7jn}C0U8ELKv_NiR zdzy9j1c#?!q@HP<8+~|Ekn#1v{Hpm|VPG#Xt`2kXmQ$A^V zQdsatm^^56Vgru$EB$BU7YT`S**`LezybZ81e9=?B?XKX1f6L8#p&q9#OI@L5AMY% zeG&*Mr~*8C6RsFW31KytzpLn2GQB6Y`_lh3xVEt4o#4)YBMkt-PLO@UDi{amjvn4K-6K})YQ=WUKMS2PZ@|sFv&st%4REOulYbH z>2IUzY{8Ypq8bT^CX9B6i?x=i$d+)Qma*8oUMhZCv0+Ip^!sqOI>?IieE!FM7)(>K zp#bV^@7f-&s05k#a-2XtYo0Ab)A`>GNs^BFcIiY=w8hr~%5%MZ*%IzT0dLKI-kg*< zabtdJM^C^yphD9+#7~l&NW*-%-9dg~I3BY30hp9>?w?%=7zEMyaW&okZBjoDjl0y< z;$zi9-eljwrcYaje|L7XMj!EGybAoe4}XWW;%BguvfhZ(>A0pM6dKJwBGmh}P%Wb% z>Z&;C!Vl-Uks=u@fYKzG+b~ysK*D5C8mUaxK7<8nzlM|1+B@psKwMfWC zq99h8I5vSB3s^5>hJbL-4Q#Sx|+S33!EG1NpP z(x@!h9981ifPF^?{g=$Y%d}iem5F_L!#Q>H#}5Q(;UEuR`)Z(Q9#5}83L!wPjg2LfO+w;TZ0w$=uF;jz_RQ9s^BUIn z<3bT#=Htmo;Db5>w){jOAn<-Xb+h>LTG?CQ%y%`-y8EOhhmXKK%q&$<{WDl+;L50+ zc3jw~Go!;3OEPt`7TB){I;>;g<}qsgmM-d?&FJ)!w8QwviT3nwedh$Gc)vS9EC9L# z9=p;CGFA$O*G!tiax1jYeM0)r+2LH>&JiL+&f0p-m`$tUk>+eFYs(g`Ois?l=~@p2 zHGyLw2|*Pfem6duJ|s1 z>pgKFBt*-%!&ZmP2VdUq=69}v{C*Hgp!NcS?;A0Z#i>3!zQ(SqU5L)nZZYuhCU7%` z+wt2+L>*wPba4FZahh);m&9DgjGp4vItJUOFSik}IH>ST0TJqOK31qUURT0d@0RL~ z=KJ#@oXAi~AhiROW{;clCC|+@F>j~5ZdMQzno{RHZ}<8EPqYj&u??_sz3P3e$4vv6 z)i+t5>F#S;PYuQ%xyL-L(77_&WR5l|tdl29+6I+m?-^=~v#~n`y9EQpq(CNVdPe`_ z8zT{*Q89_)Wsfb8#xMd*JrH1=K5R5HjVgRmc*E<0(y5oy|M>i@zU09hC*b5d1*`Wq zBR$SF@WD+o*0S>2!#sAU0HzPNI z;5IHF3bc`9u7?#4ZSLQiy!Q^gvLCu|CK;x+7t!D(dFVN{I9IK~$%>+UtGAC`s5&y7 zTWp!0{mAo%0_HBx4?+6>AhwoC^gcK-)z$*^>wAEfkiTMmQH7kg8psf>e0}u~po|PK zZp@kNZy$(s6pU{qd?`Eh(c8+Qh*6QJ)1^*SfDjnZMSAb!s!(ltGYa_WUd{#6z+*u` zYJ38vH3E1aT5-skh@tI_xl=1ihIn8+xW3frv@o8Z_}M)`sUM4mSm@}UieUA1kLMKH#AwdKUDB^K zLv6URUGwmZ^96E$8`JKE_&dxKo#MAzwg~>QqLMed;GN3vhl9T5ted^3O#&%_PQYBL zC0)5CsIvvQ)!KG5-86{ek^?`24bnYo0>$FDpT69A&hq8M44sc2v)aRUOHtT!H@)(`E zpcnS?@_9WpKM*jxx30Rjol0V43L>EbB*93s63}LHcFcDGOma6m2oZu2pf3W-=eiaV zfFvg_^*=h&BAPdKm|^-{q6KlQatv=B?>r^sNiz|h@dT(j4j5+xMFJCLQ6#&7_}H0 z?K+EWXp+YB?|@ONH6*>ZC>f8zgy?&l^r9svK~`L2-<>XL5U>6DZtMnX@=gz5d0H@H z!>MnOF~N0rB~pTznxsiWZX&9`>WUWS!a41Ng-C)UCKMPVl|J?HeUxI)tI`_29dcvliZpJBk3wWf`5umFw_WW|9x;VdhH0MT8 zD3}5OzZ8T(q!;b{9C_oMK%z;so-;kmJyfTY?*de`TE`J!7IXabpEPaZqO-@iXeR@^6jO+HEFhhNum^m@We( zPak$~BO`y!0B~D0KC$Iu#eHHlh6Q1qiFq3m1P~@`?`qlGvZ|@+k~3POOytdfVa`;U z=(?%E53Vh}+NJNH33y&Cc0K{*=Le!s0R%UG`0yc+@wdv4)r!w**dZWg2$0P3 zlQjAsyJ=t4C;EN9CwulsmKiAdMMtyhV;+DfGO03^*K9H_t8|4EQHGP=*qtql3KxPO zC>v?NSZb9wGLgI2Ha8s9{R|!1HIGVPVV8SNW%=dWAQn*aWltuC8Ucc>S5)T+Wpt`< zQ|x(=DcKpSna>`s$V7R!yr%MT>jojL>#SgU@%63xc%60w$c$Qc>~~bWimY7Ts0p#g z6GT$3%W1*ggxc?Ye(%2LKJ4X2u;KzN$8sn3F0gk#--$9&0<5P7dm*UtcO|llfW5@! z`Ja52Uy86m8dF+SnErUA>rrblH;;--fpN;v) zNc|aB4FB2aq=k(9)bZrw$;{UIUrbv*4Vr|eHE$`Zcg9J7-`Fz9E7E;ApWq?iUI=G7 zf7j%>oSFZ(r?bc`@7I-u@ja<9|BIoE)0aHarectsnwnaNSU#sJQ=+}|?3E$*ulM;U zhEZctYj>zlNlO;i8ylTN^SL{_m_nu)Bfe_|-KPQjWN#UbRYsZf*6&*eUI*BoU4B<8|xq4={I(IYW(W@HA40FS?nWuBo zSe50Au9*V(bdGWyE5J|!1gYB3nz@BmSk=-(u%Je|tM`%MhJ}PxKiwI|G=*VPE>cK4 z_&toqeCH1Lj(yLY_6AQzbI$+VvL352lHm+#pLoZA60vRqg9x4IUhe{Q1v-IEJl?&vhd#u!@P~GxX3&rHax6 z<#e@t)P63aJj)VF_=YWZq6K=$z;eU7uLlR70H=NeEP&u|fDR|E=0GcH6MwvBt75fIrtC^9a^L(p&T&`Z>=wd4D_y7c>?;Jv;=UTwuUefO)zB zyDY8M;~9v*?3~A><#fdSDy7o}H&MXbzqfKz`-aD86;s>rBfSQ%I>`U&z@of+PjUop z1kCC8Z$$>l;zhm>=`?p?ry&X`FZ`jI3Urzn(OI#thklOJ^{&9$$P4i3#ngNIl#f1# zzfFg-rR6~j$SC5#4?&(F=76I_xv{bFQ+p`!Ub|8Np>VZ{$(=`3=8RNpk30`>hW5hV zx#{6kTEfiEutKEi`$?>EQqQ#sv51hYF+EF4xa0T4fa#_diFIJ}_x?f6&DGur+jOyT z6}##R17K+@7=K2pI15gcJXYt4@7%4NvrTJ5ItwKwN~-SxuYV=v_@5se8jY>QF6Bnu z1dUAGfI5FZ<*!Krf8bSUJ~%jl zARBI)+lxE(njAM{myTkWmi^|e(~AULTT-TEWx1SR5btjtnmc7)=O>0u6GI*ACZJ-x z*WF{JfD`1qiG5>@XAh>)LG$P&1$Ckg)V{Do4pVGElY9rf8^HVWCyJ z!LO;OMnDDr0EcxcM1HFMfgzGejmGrV)S~=>`??xfY1@DCv3H6iqnG$KuE1Ckmi&uN zXh0A}!cCy^T74*%A@u8Dkv?(L_;Dn95>7rofhNs1zU# zZqg{{!DS7>P{8dGhN!NptQtGSBaqDtJ~=j+*RAb&?*i9mwc#hpAOef%M=ou6Q&oFy zLK+INK>LaYt7%X<8ViBe1oU4v)4I4G23#zc!Nc?hMjMD5dmf;)uhY|IFX?Z0(+S%p z0tOdYG@D9Gxd2XycGpbkEfBZ+R{UwPE9xZ{RKNIwOT^RDQ)0qFgdA#nYuP9Bpx|up zsq{Pl&z$SVkXYOd3oIg7UR*m)>{HNB>Ko{F@*{A+%lzeo?KgJ0YZMf4U_Aha><167 zT$O(IPR4Un@9g|!OE$orY|Pl_*M-rOFUlIpmaZd06Hin*W( zSD-$31OxtJ&U-D(kItfVPwJXtlBGU1{oneEjgZ%ZFP>*@osg|DX-e&MmPZR{!dT6- ztA-iu=y)L!k&8tl+D&+-WKW1m z2sTa^5aCL*Kk%KDEOF6x1wSITEvQdR(6or}=w{#%1U_FN_}T&E-Mf;KySFv2C~e%d zh<1hMUrHa0vk&b>ta<+dB~#PLV^k*x7mD%M-V7q<^*Us^I12v&e7cA%%^Cn(rY?op z;jdrYH(asewMhUciXp~9*0G^TGsD>MJ;yxST$!>5tcW!OkA0ar_1Vd&ZhWpWkT!Bh z8hxmBo^y)5D*1h?#qWbSpi7;>zN~geUfB~PKUhwJan8 z;?9xrJySrd$DY=2k1c-eIE$RT%ar0Pou)IpL6_Fh<;v7rCf4Ixza*wi-A=&lg+OxP4edd;&)b6V zW(vCSh-sAiW|$j!7@t>$HmK$KC1}0iO%3(BYBbi1k;a*!dEgeC16&B^E*qeJEd}YL zL+Pj>z5Y|>cOM`0z1?mGS_%ru0hlMClMMzx8WZk`rr&dhaw9_<{dc-(Wv2l74b*UN zO#Ohy=YHj!5k`D2u#$V!;w>Gs#~|UUP!dJ-=S3j%Y$b#mHPt2)s8tYqrDZfmfvlfS zo&YulS{?(T$vr8H#m>m3PG6u?-~kGIgayb^(oX?#{oLC6))lhC(C-7Xrwin4?Pfer z>6JIQ0O1U%4C3UZEg9{}9OGr)Rhii19DD=(b+EpPpNkW}sDK$hf~?`Cz+i6 z96$rfNo1{>nX11TkrLus4SjwGz0qhz7p4Ns%XCj3TEm$;4Cbd=lwuzg$-`L&H_Pmq zpH90vm5kx+4KCw33uiQUB@R>>h>z#OaW(-&SQr9AHtIqhd74pJ44_jjKvR7AE&ywdU>#*B{8wR@BC{& zuI-R3xdkWB$hvAK(1qRi%Ct?#9RKsoQ+mLCP6pu-pM#|;2h7(F9W+l1GO#b=H|6xu1h4g<}zWKk_JH_YCqiJxCvOA+hb+=S8h-e{smW22hM*v`)9mKOg^2> zIhkJT2W?25@%zK2UzoqCLx_0Q0(eOWU2xs(GcIRe?Z2Q{Set-4WWURjG(<=I81Xn7 zICi-+1{NLY_M~1Kf&TQ8{?(#juZMM~LV4yXzB9_STvQM4_LU^rpujC-akyLr#T5+A z52UP{zkdC?k)=5jOeSF?1+_|UHPr|+zpMdb-8ray0tvm38yliA*KA+t?%tN!OB-5# zw#3ir90p4QcRMJq$t7d-u+u+3N9w$?d)ynCfXEfLMg{I@2bcJVdOqYhZDxd!3tKY;g5sw|WF=e#Deh5i1= z%-_UAh?p1&{&1er+mWG0U8brNvd&zlZ5=Mv4(%f?pF|6{Y9rO=i6Ovw8(ZLR@&!u{CE4R^9 zp9XV_1s7@sneHPa7ApI8r>7Qu8M0Z%QdnV7hX(U*Z(&}2RlQ%sg# zbQh>=8aF5z27Qu(MX+Q;U2MQ@9$xHQ_p%*8Cl9XtfqQuKpRT_Yn)6yB^#lx;A3j7f zD9+_UT9~+wZ55<7W`r!EY5X|#D^V*;U^VI^&dv!XxJWfO)*XBG#CFHO#qZ=m_D3T# z3YnHzo}5BMeG0Of4;Nsz>+`^sk;i+-YdMF@hqxw&woWfr_KurdHKdYri7UNW?g|uA zQETSu(yUfX4Oy5!$Y*yrDA0DAYnfq|^elT2B{F7*v#8cjt(giG&~UK_M}Idery`YJ zXS_Cf$*o|Uou-$pd<`(K*C_0!M8MAg6Nm!v^f{4(#oVV<=!#VC?I1IVq}2b~=~e$a z^Mx$}8Ug>~nC0`{n}^n3KAcLw)}3)TBm-Ac(g(B4O^C90A+lFSMzXWXo++|78Ici^8KIQDSN4|7 zP}xWJ$cp%0=k@;lZomIty>1QXJRi@;bzS#$As01eZ}H?Yy!tt~wZKaL0|^EE0%M?} z7k#gWG9xs`%t6(QY5JL226?r*W;gso5^mOpzIAKAntCoaEDlP? z?v!K$G@E?}9qEHiPp(HxD(Q~c?~^@7e-5yTee1IF+dClmb$n&vwu3;oLHPzYX67GP z>2rquAAD`y#DUq&NIwL#EQ!{h@*6St#mBc@f1dkljHMOY2N&8iOpvM;a}o&C6eP#Q z-ntQb4V4*PV*%az9vkXCJ)3?^~pbl6)q{u(go)_S!R&O~tdDykr-q z6bQAtK&tph_5U8+NsXRk38E)ne=&8vlU$Xv>2T8cb_;H_0;bea`O6jagq2|;OUlH_ z>$Y(O?V;uLRk@W9uD4kSHr2#o>aWOXVdRvs5Jfe+j3rOV&FxK!lnU&yZ_7UhqLCl0fu&0-#OH8mA` zOFajvsi{2U-i6vzUZ$s4W-G&^ANluX)q&A%tLy?gd?NZ{jYNQ z?3(Gut&(pOD&bsJ&a)Zz8CVDz>opuS*H;Q0RW{*`Ub3LxNIC0QSkX%?xNFZ(<=!s`6kr|M)D|R`P)SI`FcLKplsm`A`KExGCtAGDWh;Y*q19uhf3&XS zE{x@G7eh=0{A9&Ck2v~Ba%X+YOG``X+0ZR$5}~11F-6SnxjtOd8G;4zj*U-^pHfIL z@u_(1dJcq8ng0pVm@pEr$Y4l<31H9l)r-E4pz!(NOX;nuu?sfc5A986f)frf)Ey~f z{*?CF4ZJ)K{9$=u-%g^CRlXD}7y#S`xtnQ|E894$eOR3J za!sG*Wn^=lU6bWY@zdp`p_^IW?c~xAE;?4E7s8#3Y46qZHEs%R!0hv+Yq6h(#l435ZG9nJkEsn?}ce2*#yk))PE22O5V@NN)j?+{pl?s-?m`D@Y`XjH>JXyz}#jk$FhpGP0H?_NA(z+blgh{ANEFP*H zN)$0#0Yat984IpoiU^CPPX$_Gcn(>1;r}W(8rZ4N6<24x)9Y)>>WQyP`1V&@_mPQ; zOm+MJf`7&vuu*h7{nVI75_Bs!5N%cVV@;OSlRAQA`_5fVK>0!+0L2bH{OrJg^3&OC zuOHzxnD)z!c#hnC3^u?a{ObKzLI)WNgN_dHCMxA?I~Y>&qObtZ4VZksQCd6a;b24h z@C1MVun`#|WlJkU5pp=A^<&g~QDS=A2T8To87ZU7Yi_hfGg z{jUFNxx6^LBuAhXEobY#JIyF3a87NlukYU-Sqg2|@GP=f*UR`L^Wtz@)X3rkI?4r5 zqmzlx0R@2|ZaU&DXrsB}^j7Y@g!>|g4C_xSJyOSs+d|_qt~v9c+fmBNmU?F_HrVn1 z@S_&Q)3R(&-E#S2?U|WgF13?G4FKU3({=s?&=&T;wOk0}y_}@xj<5JpsG4n` zjav~|o*ivX!q@TxkMR+H%|*qia{bX9+U;sm?@0xw!y zFAhJ~qak{5_swNIFY8tycu1+;qLlC@ft2HZ6*vrY>g#FIJi*$2hztQ0_1qW9(1AFcdiyVaR2wwzcEM4=DRnlavjg-IjCQ&SDLPdx%14>Y>54wE zBaRf^HwX4+R7fVN#TdB+3|Yip1rwM!C-dQg$O&!#Mr*aC`QHAL?vG%> z8iu>Owf`V8Rxn;kkj7+)CKM1KnuU!XLeaT-ReY~_-uGe~Rp2H`{7(z_{c(0P{X@0f z42sML?yKd~hwr6FnuWZt4tR+uTPWJD&>+5g7(PnDv|^8my$EV|sAS{D-nDcmWw*Si z5^eA*Z`b>5tF{LeKXuNt$t0MvBnjXJalcPE`#NwxDSO#k>s>OPi@dacPMFs;2^2B$aoC9`Iez^YO^Y9Ngybcl(8@mdO8vwnEyStY3(_n8n=8JUx7;QpUu-wPUFS2B>daHB zoC#yEIlMb}KK72vkW;Ug_I&#up-sR73vN7(=qOZwa8s^BF(Z>>PU1 z^qj3Zg%Y`+hRreJ(I-eCX3W9xiM*>cdXW$*%65ICFOdfrDHHPxy-spff7 zR0T2Y!VMMnA}u5W%G0aRIwMR(RL>Fuu?xjxLXo4>@MhS=!w-6K?~>bL{+juEm6XsO zJUYR{%G@}%E4os@r5bzpHj_i^NWAs1p2uN@9iCsn^aEF!9Zi4$?up)4pwOvF}%^$+t^tTQz zx-#yHxN27CW}z*vy>Kc$n%kvX9q2M!!;6XV8gXP$nD3<*L`sRs(_9@QSyde( zIl9GaSyr1$bVG$Llvl-6Yzt#sDEF}b9QO+;?+|2htPH%~sFUEW&7OA#@4%kThbMi# zIXQfK!r2B}ME76-Kl>&$1C)5agNp9P>aN`n9-R14QZjZMVyD?c=&8}{b}U+?Zd7GA z`W_K9m@R$zgHht?^6bpaIk+Z3AJX`D{X-lxvKM|AD$bVba?d5JgwUctpi^hK z+htZCnk{Jd#b3sV->d`ft$PuLw{;2n*)m4zH~ZJZZs`)OC_dt(KW0V_f~2Nmr{%1H ztc_~!9vC|m2ZcHfm36srF$p?kXmH9Lm_O-wX`esZTeRSivoNEe4C?_T zeHP6Dkc9m7UW&83kp!7AV4sBmFu0^g?rjxW!;9zp&%`}5zh7nVr2D(kusGDS7Eilk z$BF(FZZllOSFg9^a=?f|D8=W%e;ZbT;G=fMMVu43!{BY7F({$|1UA6QEE(n7UMID@WVNIvNj(jbTTMH`O_Op)2p zF0`P0NHR2+7fw6j?au(w8)y~7%ZABV>a|=?=-blod@>TH$7M)qn3kT`e~10S_=&tw z`uGj?G;;>f+E1eKFIv@NC3I+@8JvJd;~J;Enc3NY;HK=vOa!+<*ExKDmDzvjPELed z{%yHl<|lmOaW=gg)GjU?k5Uph(<&my#?{tkMPu7~MGS4?ks!EmZ49cyBzc~NyQXKv zbx=0lRR#?UiChjaDGZ?I=A(1X!PFrj5L{+@q!Y><F*pSmoWvPGqcefV6UCHoGbo1*(|zNEec9Y?~jp5fv9Fw<~6#UqtdA;Dq} zW=*u@{2KM!GnenBOUWE)j_Z8d!>*oTB33dx4yVj6ThrBVKrWX1aQdf>WpKizx6xde z{yX}z1|-@M!nMF{gPEyqP8-8S{m*ZAEDNkQkkE<4C2w)0nL)~T{s}4p`F$}A&r7^Q z?NdJ~{z(~E{KL=2h3wrkVLzmCEB-wQw))3hZbHOzNAZK(>Cg1x=#fv-o55vN^V6fF zEIYBXTq&&Iov0ux?FmpyIfVGeGL_#qO>f|Yk?i_s3v|zeSM{rnvefx8m~ghP%e=Vo z_JF}<{%XZ~eW&VU>%UhnB8+1m7YK;ce9Z&Tcvz^vf zA3!|OFnt6D!1$3d zcyE{6uU5phoHw_xiqOz&f4uA5_ln(0TWLPC!|?IjYC`JlZ|W8-2W2`Sov?Ftt?l%q z$kFanq<*xlYOVsTM7>(ZSvL`3{3BXBb!}o9f(~O%68)_vGtRWlxJ3|fR^v?h=&@(& zRe$|djYo?a%#`grEyHe-uN2oGVa(fwlAMb-IyqO{E}{_4*{h8Fp#~}g;2b?h6{$7e zC4YmqDK~~6SWLJGT1RL?;n!Qw{vA$Tezq%KZTG$k@&ssxnUNwGcEKp0fL&65!Sh(- zRK@aUh7o0GgD)jroYu~5Opu_-^80(_ngWe19*Ml(s=lFOhfL$-UEO*2C z{a7TXS1(mC)_;?wn{+?8?;z#7UQJ)_*PA+G5 zxc4xkR8jG-5()NC2`}e8sqqankFV?0Fr!^+2IWyaRnur^%IvOBA6}l5%5~f7dn8y* zG|Z|*FLoDW_-CL3)0k?2nQWdk3$^liB&%7CBNY3gj>A+>;FRuN26Y)^hh0`#Mwg!f z`=%phsIpVvptTS?rQ-SA$^j@UN^5OlG69BFIhIBi$g+H(u9YPAI?0cHV4SG|C(?oy zjg{Z`3Y%*UpYTDWyY#w)_j{)2;#td4=(Y2CXSpbMP!A8Ki@N^oh+p!SC0VAmB6t{} zG1mL&*^fgXIb=_qlN>S|#AwD@D8adT((3|0Secfq4d#)}I46;8B?Skom48c*= zaQoJ+F94UE7HYgvk!1SZfL;A%M!e50q|b7`h`D@kgwWa9IbzNtq0ibJgo>vL>-s@$ zT5i7O;;sMOS9_2BGe%IHR)b*YaL7y6!EoP%L3?567tndGJ@JgE!^6WeV)2+(>cc;; z*t6(GnZs)Y5?u9D6D?kbUc<=!kJ-y4FHjPe&^k0c`7LI67o8|k+y5drXv)ozGs9yG zJ5q?o@?C`UEZPV8}wd!`*6*+PfFL5B9hre zdm?1UJKM*esNPsNRKP3h?6K?lJ?T z7_JBZXHehUKi&FRIXpw>z>BR*LQ03tkc!=5Is6XYx)j)FT;0_^?t!j-v|Bv#!>5LR z+4tX!>XP;7Fh=RU%I0D%&F0qV9`=23)Bf>cay*M_`7l6(tB;VfoB;A#6Cg`%X-|wa za1A^dH#~;>nvZ`r{;1$#9o*{v1+VuT;dFvy$(IPkgh<>uD)8d5$w%!c8e;^Hrs7=v zmguwJ2w1eEq|SFcYB>vnEojuO*<1KlFTMF*TNUeat*x))L7nL^Tj~(k=gVEh!MxdZk4ct@QtpY=zc0iAG74JUoPw{B zmVBcM)G~e@VkoSK3So*)J}a45?j$R+k|sPRN8pvPg6%cLPQ=I{^I;wj5hb>ZK#dC# zr3wSj{akh`N2El!&z;PC?%cmy+&0IgoNm7KH4Y1zzkrlac+Khp)+5lmf?O!S(Eb}U z63BT=U>dIiJPC1+Q@jHo_1@fR4krxj;4)I{u~Or7iFgY)nla!^YbGz!LyMTG9uSfe zxL0ay_seMgFWO0%3yBFSvV#BwG;3fzIRNM>U|d^Sycwgz4gawj2uSC?cog1}C&c4M z1ZT)txVE~QQMPNTi4W62%n=(5DGIEzXgH#0=})S$%lAmn`&4`;b>TXy1RXhf8weUJ zG^0iz2ydhxxN(9#?{Bx9ZOgNB!~Z2(x*FMH$>R0S`aY?#^|7 z2GPzX@Fwb=-EQI%qz~n=WOVUiM(h~5F&rt_?H%YWMod|!lQ8YQzNn=~#Jx7Ww4%cZ zvE&udl3kU_Z6;7CG+@7RThYQDtn4OYSs4~R5BvsWX@wh&c*gAMW` zQTJO!iIHdurf3>-dGcutM0C|wHP5iZ%s|Plr%o`Uik#TFLQ4`ecuHwI3q+Zvh^Q#F zIvqm~3zUU2$k+m{M3e;$8fXOl465N%4AnLvea>vmA^cH(_o+B&l>P}G%FN1IDn~X~VL3X2rT+xH# z2h%F0>>&5P!;o`lrj8vtgz1C786_$j3tI&ARe_Th@n)5znzYS?eQyv}q0B@&iA@7BG4Rh)v5WX}zoO4n^b* z`lcd&g(_jT3+3qZvlE;7K4VTE@~8}2V172Z)nT{@Na8K7PnEhRp1&a=t76)+86H7& ztRR%BZ>3PPe|*`G;Kih42?M(PyDOBA@CO|5`nfb8PWq$k2JbW(uPugb|KBTBe|v_= z$3%*kpQ?gYU*TTVCnO|wzGTu{zEws-ag`-jr$5_ozmLF{Kvjn0&Csrgt?(g}p>~4U zGIOGS8SQq-j%*dz^1?=d9Zwtb8tGdx7p*P!eI^p}54{MkmV@bEOx z?S~DD+`T2&YE1g@^BqvpAXKZ;0*-e518e29<7KIAhG6JU0(_C<%R3gJa@OK=fRJn&dA_>>iJ!kCV z5mqUu!7>}M%;7M_B}YtyP2z$s-~z1n>6r%7Zi8%tr~`GUH3AN1A}=@{juyPtJikRKancd4USOcg)0aIX=R@F<*4nm$L{?%v4m~y%w%YHy2$3C2Z z`^dv_@~}2CqV&5Z0au)V>Z-4ByYub;b-d8Z%tz8W<%V!j)j?+NE2xnGdIguTWJ1cz zwLD)Ay3wc_9okO(UEU9o`}JB+IaAsa1|jEFS!}8-P1v4YkVLWKvOQ>g>(YZ;m$T=a zQAe}E;MY4$&)IVmNM7Od+kP;tN)3^7S0@!T))YgTWOyzuXyZxVYyuuX5tJ>xr4)ZV zO|BWF7Fgg{`IUCpd_~j4=ELHYcxDSMjqthda}rYV80_ZSVc(qA1JRrh9EMJbIe62w zg985|;3}PS6!{e3yl+W(NrHfqMUyi(bam?XZQ+{3IhK5)`_}TKHl;k ze#fM0kbb)^WR&2dQ9a)BR+)jS;S)F93OoazBF?+kJUYeLDcmZXW-?B?0z4M;3h!!0 z%-8@>2b1afq+ zv^;}3tkbf*eJ=MM9poMWi{VV_sm?6>T=qNuhe&UiS6{S*nq>tTuKX87%9!_v5=0zp z>Msta;+Ouc2^jI|mvvrl2{E%@)2fP?+IoT7doGUKiwL4}KZ+f<+SaVX9kUZD`Ce1{ zyWxHa^E|jtO!%mQ!vtSf#Y|-XYYQ55DK2-dTfnKkxt8=P5yD`>VX+@tUQl^9pfX?b z;nQ<`(JZjo?*tq-g2vZxp*{SK1auLA5Tt^rk;@d)V2g@hjS|HQwpjJV)3hn)--L0$ zsK8-l!q=RJcCWN(mRW6A^zn`XsZ+QF7)==cr+!=OrQq?xr#2FQ?5Jg?1-Cwh{uD={H#sC}2l#49$Fd#&zx+I04!Lb*V z8ee>efsnj7Su!r5x+(p{DHSu;1tB(l-FW?Ke3xeQ3V$9la(XekGO&oQ+fuLrLN)xY z+oU%>R!SvX@*L?9+lVPdtGKWehAu#agc7c-4B%K}?=^qX#&?~p@CVwXP1f508o0(W z-JRLGdQ{TK&hwr~4lR@0kY)$F>EKY0oKvf%szO>xck1hQi1} z8p_8cGw@Us|zq(2*{F2tF{Iqjj}yQQezKd@}jdz=s&~`zr%IeJDPQiwa6=Q z@S<~2BG=YlFjKqmcE#P0oVq6*0aCMS9{e!j-!urs=zTn}CwIkAzd^^+qFwU=8^*sp z%zU?Q9RS1hgw|@_$F#1XDs5l?>2_JKzj}zQ-crq|iYw}0i5A(J6V`*g>k&s*D6T1|8Fk3&iRHd^V-M&+awG`FSJ zvV)3Q!HjX7G|rO!B@=BCBqbf14B38+JX_nvM_9OxoxbM*KKT3?r z54&2FG?OuFvLJW%o;72efD34^Sh|oM$JEjEn{Ww-fIU!%fo){M(7yBguA?+;gI4q&BcudA=YyRLWvyLi0{wgpy%{6fDViDhEGJEJ9F0^FxLOSmRvGUi{ zaM%{m}BL*kx?397k0yD9uIA3FYSxnrE8bo1O?;cB5^<%ZFMjs(LrR@ zE-k0FQwL2U&kY7+3#nkIkYu}q+^;jtzybt1c;^UY@sDV?xq@Y5ofAH@BQ9gT1T>N| zpM1M_6hTOT4#78`5F8it$akx{2T~L4!57mV;wP2u>gswKX`c~qV|a@0)P^i^gj;qF zXoYPJK=abu-xG535C3J4Bjue4yoB_ib#&0;?HD8y81l3wu^+?I7E?1u)Ft5{0RC$G z@BCmy6AOWk#21AG0Q6T4@D>QbFh%()v@V}smLNt&G=Qpj>6h+#pQ8YW1xv5g=#b9p zgn90*Cpm*=6l{~!WbkgOKwyU-(5p`hux(ko%gq9SMzlRXn z@9qro+I)E_w~Utkdx_YXUyOXdA=U8*{12QGfhwFfpfj+u6~M48A6eIW5ygQM%|0xP z$8bhc|4{w=hbg*5k&1n4Q*-VUdZRK98y>3UE>_x@a;8D&io2b*6z1;G5<&e1_{(;| zszBG*L9-{5;FZpa7@u1mL7F@#SKj;EU(u|(Wx0Bor84lOBuFF!egEYw)$k?vl^{ui zEkch4{RD_~P>_Mc1=2zjCsY!pyksCOJ%Oz0hR^5q5H(mS&9QicyOzRJG9HLNmp3|i z=;s6mMHBZf%1Iapyd^PEI{z2C<13W^yWozU8g-z~4PT)ta*FP4efs9AH2I-BRwpDV4`JV@mHiX6j6(gc^BhsC8q#UMmM!fgm^79P!`w z1n9TWI3fp@C_#_!|DVs|H28_sqKAZ?0V7jXJ70_?@4GCNEXOb5kG4~cWWJaR0~JLl z1d)Ue<`5Zfb~UgSlz@wGAyKn~Q$nja#%pVQv6?}aHEw$G9#VT(l)$`PrJFTTh-$^B zLZ_T|^s6PSlS`7UL2~XU*k}sv81Ey4;csB&z)rmDte2z4B}Yv3({mVK=K3J^(cgd+ z(;PY$x0aCGUtPAI)gAEjZCiC6e|`5okr4uW8lOVw7YPnIqWveWy|*9r?JJO~WnuYF zl^7*}9Z#tTbYq-blxJl85{U&{*g-J*T)8Vc7867+1G@SAnz7obA5wZpmSmS*Ej0~Z zfvFpyMkbYM{ty`qB8z7Zc}7n5?9J?-Z>n z=lM}>Yn&ex8n-Yyq)wfmC`ocqwuLRb3&mt3FU8-Vat$zi3HC6AriWu!LvCwIvd;m2#EM9wu2$p{E zfY4q@PXaZ4jmwf&^sAhhXAvPqr&Dj*uA@JU^{)J>m!g3ee&DPtcyoXPr%uK-^*v%5 z*0?}K&YO}T-WL#e^KXMM85dcb`V-1IX^%VjzOWFm@?{L2Xmu?NYcN#;q&k;eMpCc| z=_y*j3VK*K>@>G-8c8Vcscw~rZY^-{`=Y+nQ zGcTMG-qHT(HxgCqzS1=wRN;-#jh(wgDjHG9Bf)TiC)=GG!Vy|bSQxJ}-^1hmY*fu9 z^mv#-Gt;cN&>mPy$ixNT1yI@qR#q=$lX@w`U_XQ+m=AaAP7En`mQ0z`V3fNdcx8eL zmQk{R<4@O`Y8#z=NzBm0`GM*PE9x*)wlj5!i_k&lsQ@KyR1j~W4I**jM(8x#s+h}H z;O_9q)FhpX!oN(N-eP~4-v8%WjvwctCMeFigsDD+bm7gsHt%{t>25N;)^K0mfr)>V zPgMcPgDEc%pjJaT%6-HMhk0Xj^IdcrCk#A;2a*ZK>F=l>a-?cJ9Llr;HzN*0z+<^L zr#3>|W=5tFgSkd87ms5`Mmcgu<{5?k72YDDliIZ7-$!SVI7}@reG=zA`2Vl%nVC@q z>=_#Yw6=k-ukY9I-}|&!wnvyx7YL#%1T_HsJEM)TQ2kW!=Ft=(#2d&?K`cWZ%Et+; zN(cmGupn8m9;sS@t@vE}@<6(0Az~bAR*f(mM###Ao)w5r-MZ9$6}EQBOc}B@6rn4C zR$Z{WN~cxx_8S{`TAuQ5xRk?dTi8z_6$QM+AHfq-!$G2o$7~4wVKBRqxNMb&+|?y2 zPxz+9bH`JfCzqVEwHzB2MO3=AusdFZz**E|J{Y%wrf^uAj+IBonv={3eVq;1vYP>H@`IN;GaDZ|7dwZVypp|>jT8%MqXEPscG|_ z7*c9WVWEUPKJV);ceqqGv)z0iK15i!$`ZDIK0kzozK3H?i>74;wtKpy3RwUs(jnTw zMwV9PjS2Gx+9J7n-~-M28nkC`NMkt)b|I~3vmU0;V{0MW2}m*ZR$hfxH@L&oL44izY2*kH_WWJsw-d z6NBQw)M&}OLE>^oM#c$9&@gF{q=*&qHV{M$M2~tg0@G1JjX;B-5*=L3!&mbDMz-pg z2if`&HGAPO+NpF>HA0Kt00QR?v^()8+k1nj5czy@2+Z67B*ze(@I4WCQ;0OEU;f_n z1(^hXxF0hw6((5_2wT!ufHp0jNx@jSE=IJFIi`N2<0ULIa<414qKRmM0L3A6AZpBf zP`8M_nSXV?-Dn;>GfuH=Sakr{6`InU{P4MD9j$IUiyq+*gebRXmvfis+aSQr}{Te`!8 z`|)(0VnickEX)A-<>kP5NIrZQ3n8FU`Sm%+hzS1t&h2YrK)ew{G78yQ;TsnZ>z=P) zh(f-~+gg;4w+GwtZs9Y(I72*AcQ5Q0(bxilY47u==A#+i8%dz{ugo$!qh#Z9W7y}- zKNN}8-hE%9ZC*~s@ezx)!>kgLQ0~6HFYoIs@x9uS+6VUcK+INwoP8&@t_7~72nz0I z264`R^7lv|PuxXX7BHw6U^8KyaQGP(dAWh|!yXpEmFPi}tO=_wft6!RR6+Bm@GcmF zPC_LUtPpi4i!sl_XYS^v;YF<%G_~=14I^>8BU=k+)JH)wv~}yHC|KwHPEb>r!z4&! zW8>fOg6dNsA!g&BA6&B?HSE!o<{;4fGKA;!MIO5dss0Ki7ZG0d+w1GB+2_kQ8J&2O zWpOX#V(y77gCbFc>E#$CmQ8C5I*$F# zW+Yb@mJC6}bNsIf>sc%Czz+rUrwBKfM<<4;qKaj|x*N#~+u_)J-_-DIwhj1x!0< zp1g5Z@)D4|7WGCC9WV+pZ%K}9Jz6yH94rF4BU3P0YhsL33!(|>!J{`S5+~4o0=l?X zG1!h`W)k@Ca2386Y9JINJck#AvrpA)z8&%h@PWfaMRu_*)aq@>lCPKVkcLf(VHzPe z;^XcyW0_gijHtoGEtU!zmBp?#5LJx=!hzlgsqV*A*EF3ZRL3m&>%l^%mFKi!^<4R& zIDb_*I%R^fLKf?y2^M7jwj3SjdYZgTJO{lE?0am0$l+C}NhptcH|BFwbFMeohkw%B z4OLBIb4m=N`|f+^nw0>=V%;mvPZje8`laLCdd!y?B&0}_ZEG|(>mvtr#6bB=M2@JicM8|`v~yo|y`W+Yh3)$6#0=6aFU)YNlNxWCI3cb(R~ zU&@V|X??9`Ti&zLKm;*Kurd*?mU18d4cu4?gi)e{)h5vz?WfvQ*7x+W3 zKt(tM*-hsxCss%JRX z*4BdHjJwa){Du_Y;x#4aEL0*uc#rq|&cXBk+KT}G*rmf3&=B{p^Q~GS#@k`Xj6-Si zktp+G{ngJYd7hCcv}!(ypAP_ z&TiaFRW`4rDIt-YJdQ(*N*kU4Jx3C;@$1_zb1E289)VX7u)0r&!~+o~ z#9Q)iM!Y8hQ*PH0UR&7AWDV(wys_n$-);Au68U~z!@_8Upx{|R?0vwWUvI)Uhhvmx z)rpFhs1(~7A45e8;;>4lL-EY&vlh$PNwyDXuh=6DxyO#eGtuQYZ8AV-Zv*z>_6uT- z_9xok7uv&Rp>ZRrqL={rfOc|Xq6=C?hv$NAJv<=Oyg_yqy6rA7#7t_8!9<4+e{6-O zE?c$0Z{&cc28R`)wj)@)e0N*zUgc+-O4*n?YduNV$QR!LD3r!Uz=ljSKk2(CDmU0O z3V(+sw9BrF-N#$9`MP};c!3J6K{H9iJyJ)4O8~US9rtMle1j`p)I@he40R5=(HEyM zhBsUunqAuXL(snSp?@|o-w;AL17wmR+^GPC!-Trb7X&A1?k|Wj=5yY*Y#Nx9{C5V# zP+A0=Ibm-&^&o1aCiMl=xXhMZXc}6Vdo)+?`3Q#6Iz~wQlh7WkLI&IbN5mm-!zWM3 z@A{Gk?w>668>9}6UmWgQVB-WEfrep2O3iR*Qs;ay=13d5j4(L^yK73!%bC=-RloMt zj6b#qS?%bq*D}QF3@lbFIK?imuH|4TP+sPgd@NQkI1T)Vp~01vSB?Pni! z?=l!KtPuwQBD@3UM~InSE2gCSan})%9sab=*rnd) z^V2an@F?~Zwv(*B&loj`n5j4f0_n-f$C=d>+&};;Hb7-k1)UTPl)q|fK9duh!LCM# zPzgAM*6Y;B=tF<7^DWH@l2nbyX2&QM(*@9*L}~chDVh>%-i4 zu+wzZ>yhtrqVh#1WNAR-3|-a>t1J>ZNb3b!7hrQ}1;a?%(S8AMDBg1rq6>kW5W4-G z1nUCW-k+Y2s38TBc?o@BOhyh);NZqW_Q`Vu0|G_!q#`3|bIe;WUl*0CA6j&;b|J2iodsx@Vb=>%1dLZck?`Hvl zCDdx8GdOttMa)R}Hqv09rLD#DE0FZ@JcD>Sb9TSI)0Hh1KqmoEVhE0l3v*y- z%jW4I2QUpvgFo=+Ov&)-Ti0Q{!t7}Xiz3q)3vrlvVDpf`_@~xN!Xk^nM}V7XU>7Io zZV~}}itQYkpwu@h=T7QFCsD{H&dZYYa$PHVt&p`N0UhX{-N$<%65AEbV}&jN+yu{( z>vr|9!loB)1oSY;mvV<`>L@WTfyd%P4W^V)s(^n!6cZ_8326`Hc9jnb2TJJhX7vgJRFhxFmK!e0IO%9P~y@@_`N}QHn{l*?A`{YvyZ7xSWp^$%rCLXj(M3eyyu8dPg{urG-NNWXKzm4Iw@MjG$g8fB;hfQ&a>e)I#?ZO$2VIGKiH9=37iENO^+Q65>4|V_?sZ4_5TH7 zq+{q^j4?6y<&aPo0Oq$f*4KZ%A{OtpRXvr({Lc9TaQly*k3?-UY>yx!(XpC`8{)sw zzM)47mhhxyAp*aeC+5F^YXL|xxhU53=bD`BO(2UF% zTA0N>c9<|S!gD}NfTD}&(8#-RgU0~LeSp`GS`qYI;k6 zLL)A@K}^hF2akTvK}j=M_sYMPcfC6U5yH%L5v0k;Xk6Hk96@T8H3QS45bHS^v^2Fq zF)zjQ({<+fU?2Op8Z+gH)qsi}zhoq)Is^-c;OF>t15fZ#eOe@I0BuB_x53}3M z=PDt|!{g%pwjn65@@^@l@e@9@q7c{xt1!~$7P*~O3>JRvfE9P-s*3r_Z5q>nQyY2& zcvIz}254w#z_J_cZa_Y%e9s}7thC&!rvZTgZ4Bvu25bak8b+XO*u`%8!WF&Mf%b8x zg=bQjind$K9zZdzm7SCo`gHGI^I8KTXq9<2pbk=a?NfUe! z3;CyI#SwU9^c5Q)m5)btrW}WT4hw}sb3wE$j}pLAzB9+HP4wb&=kmhUAILK|sPV#c zqbaCW@XDh_T49@5KHapBH~C%>8z5tzG2p%kHaB~uzk<@>WQSQ=Rg`@z>U#X{*&>@E zfz*d|mgM;)wOzxSN2VEXu3Ngq|FLn|hCN0Oin9B_064Um3KC6kzuUMIrmrE}$UDI_VFNUuo%%1pV&yu7QW*B;JWSQEH*F$!@cU>GEXB{ns zU~?p{tScOuPZZ^{YDFdm+o8~Z?}3f}zhxxfn|(#y86rVhasWHKV4YQdzC4)a-=wIp z84XKco|AJFAj4ukvjW$0BlXT&eo@9LF?I;hluS?GaWtg5T-R^CquO+oNg{_1zIDzh zbng%Vvu!%~j`&syf<2jrlLraeD2!59B3QJ3n#+vw)c)Q7@FuQh2Sa_gWDQ?y0TF_H7eUGiJ;)M(ok#djx99Fb%iRYCj%4x?*F})R zi&~wNwx?2r>QaBjLz(GeM#1Wb^c?#P-q@Elv=Lt*4FFt%N>7VQB1*8UM?R;R-h&l0 zQ(ZqA6gI9pU%|Gm#+e24p*$^_TGMMM0l6GWa@e4J5B4vh&4pB07z>$6V2`d3_g8Qa zKQ@rbAqeGS0ybszGNuW$!Tv+28U=e=}aZYI99*tmsy22cQP zir)`3gLJshpd2>yzRObZE?M+i!zc0{_ZJPZiyv)oL5u|g80?DYXRM6iKXBp5f2B`H z&dM~kG+gzE1`B`Em^pct_5avL(w@5zCA16@^IgmGw5PBXNCcA2cq>5UIEG}oQJ}k& zDU8^K4CY_wt#%?A)y}hK71n*e5ED5pE*9T`-nM!F{goc~5TI$(KIoNt&{(0DC%Px= zpc|~)FiCKQd)(%z(LwLdFp_e1^vTJ4n4O~!kU$DK?I30qBvdBk?W;a@BR zJSoR&!*S}9&6n%EZV+S*SgbijnLat*{sAG(ul%GmVJ8Z#mP7(=*Fh{ckGO5o9wzk` zAHwujCKpXihTM&RZ+l)9<0udll#9CA(i(l48n9|w+gA(oIS`GX{z2Nt*8#pp0=Ryu z&!&5%Qkww9)1-2zI!8MdxKdOLFxr}-=Re9%{-(e>8e4)wg9O9i$k>O@We?qI`Gc-H zew?-qw3Sj283&x)&lfki>qvFuio=oSh0THqoKr+V*%~%^yQBSM?)|dnbw57eoOs8t zdM$t#B{>6Zw_J}4&R>4=V!AA8@+m zjCVDJUcVFCPr{+jS;k)V)pj2~XU*b+JAf?Dsuc&JTgOQ-LIEYc7&*&APl&le82zu* zuYKNC^TwJHH^CFFM4DLYfN#pLHT0Ai-`No8aQ$rnN+}Lf0|IZM8TH#hfb_!JuT2%U zyt9^T0C0>P`->*3ltq)@Tt{oYP`5OMzy5wLxcYwVcRVQMFU+`7@Zu%+RsI3d?@nvQ zP$_`A^ffi)F| zVQtWHT*mxIE}uZ}Bt=mgpg8-_Iw?&DRStTW?`Zqz^CjyR=h@r7K$*Cd4`o3(M_0R< zCv;}xbY$)~jMUi3Y#}`K7UM}#-j71^DOqwAAs_~bIabO$HGtt$q3rRKuF-el$%QVvge*qIvZ@Boq^+dd>2eo zq7w0;-F8Jx)XDuuJx1g5oUi9R@rA!zOb8$`>tF-Ub%x0gddk3%b+gU$6b^D z33?z!OYry_DJw?;Z3jCQdo0VD)76C#IkAF}#7dj}K6yQ(P`f;h7>Yfv70-u_JNe1& zpmULeU^!|=zY(l)PrqwBkk{&d;;PE2e5f@@`TACVF&18jD)UpZuvyt`knL+m5#pe8 zLY?jv=s1B&L7X&-h^90cC1Nj+DH~^(q4%U4_Va)m`UpnSx2m)4duI^YNuG$-db;$o z6)y`ufvHWOV#gg~-BwE6J_17A$slMMb{7U&^v;eal8Bso12^cOrJCUo{Mr7|_|}9E zwP51p8l;uM{n!K2aaE|~kZ6fmIXL}Iro!Pqf;BHInV~43W#}N*!(b5fim{+M2ZTc9 zy-u+leewUJ>8qovOuN1jK~kl=L8MEh1xXPBNokZ)T3S*ByTde+Q5bI!T%>)QL54jJ1O-A%XaNofT@1QQ04uzO$#P?7lc)=UVY zd7!Y6Cxz42Wxuw%BAfcZ{Fr2B_`u*jhCrL-u+=6bD?Kf!eyw){*q8bGztu~F%73ap?aVeNQ#`5*>@nOOUT2`y9qD}<0h8Sm9 z6^aWjJyZ}E9NO4kqeOQ&XRrB-SW_f1`65Y&G+uK zl9?W|=n>EP1ElO6Mb2ydPqbq3foCeeG6w}IU&KmEV;IQv8>-cH;Gh% zwVnWDyVh>@b*`1Z>ZEIy#(?M5z%{K$*d|CG=u4S~%z0yKcGGKIUviQy7)gklqr)0x zWUv>Z@sbV@nIoT((b3P)K-CEnhbG_|ncP1K;J-;x8#o3FbP}xOW^(vc9*di%Fasgk z{#|f6t#^Xb1D*k0j-(C|9OG?$-R)L|t6dt!j2v4|kJh2cx-PIi7LPQgsnmc2uX>5{ znRb*J$JpaK@|(}-I*QW+Y8D zh#_cD62=GQSv}FsD@5wj18<6@v3w64qVe9mQ`@t=`PaXs?*5dfI4jj- z>>-f7rOqZK2)09zyidbmzpFf!_b8d`M1&E8!fZ-L38AstoQF;l*)?r3 z#Lt%a&|C|Q#_v|g5FC}UyU@sv{Tb9di%uN_+~&m)jiMs7ijl$6RpuL(T5tEAmDPr`rR*V zh83SHGV3)a^2TX0X_RJ>4nsI*$_aC>O`_MCA%k;cbJG@;js>IBU<%Y=y#&B5Cs*Dc z^BQ}{H8j8B3~^y4_EkV_DSIB+tRO@2G*CPMMgr06bsH(}pcjko1Vt|SZ`M~vDT~)+ z@8X&l^#YSF4|Av2L#;nu;VibtyX0&>U383QZ_hfOI>p7I{I1 zzSL;cJ0Sg#{TZ=C_lV&OfGXRbf+UK{U^AlV*?+&k?}f)?C6|8Qt(rC=3IzWaq^fzB zmX@mndcd$s!cpKKC(n0dd~G4%sPs~ujcsEUpAnl}M=Q}?^gye8b@q%2WD4 zu?pz{Y{ur?+}sR;4q$Q9y3cgpT6LH;5Cn1y@%$RfI1?Xj9dI?ejpR{7^UEIncAeq%~Q z$!_-Amr4Ts6Xg9>Do`FtQI;iTc{-*tB6-!s*G+^F(-4=4l;z3x4Iop2m`0`m*u`Yn zgBXtcu0!k-0k@C{`BbFcbfU)V0rn4@gwFmnf8!mnv@`MWKl+>Ka#q!2TVD^ArN#HU zitNm{jyLKyy6>ap4=}Mg(`l<8-8YRX;I6Ft;3&l^3K}&|q|H<}#i{3KJV38J?!!qWUTC;c*kR;OHgi1L1ThPEohk zZUYSs+5n2Q2F`y8w-tdIw5_O!JvP#&vgIvvB8rm{|IMB^_^I9hZQgy#&K`)#M)Isj z1U1Kp?=*OKa z3Y@YHknjw!j1l%wtTh@R(;{V84_+Ii2Q)wea@O!)I<$B^KMagM+a`1Ajx--059M`oE4BvD^H`YO?%0U%H#Q)^G}M+t zNyo_+G7ws4l3MDKK~SCxfI~BH`OT_f?cRAcq4&ZERupHW6#ky$_*;+Ott@+h4Am>I z;eMv-hwKSw{{xKP#R8fM4I^8Y?h34tjY%7YT+C}n*RXdFHdUG|E&aPIv=xTVE(e7tufIt)C$e)avs`^=H>Db$)!Q1d z_zbJ}%3CW`cPa7G>1S#?=TtUr`(w0}0J$~0Ck#XTvA{4|F89Vv>U`3#p@04Q0tVez zFyg{W4h2ley5hC+WWpb41-u+^-z>dHgNRjUHI^CPZiC z`N<0c9xwQ;nTfOyKuBl=Qk2`>roc-U9_x{4lMIP&fL?$A&knYND`=&HCUD|37N8WXuP=(xm2>DCvqhLl{QO^Ti(>X~d@59*& z)DATDfuG?6L}q>oM4Hn7B9*Q%en;{{!AFv5`*xZJcbRG68zdPai}1Nz(7_GHz>P+G z69Xqgzkd)l5UGI+Pm0=;A0x3b4=Cx7lk2WZ0y7(;3)5>*sUcx|;%WHvIrc>>c8Dd9 zHp`d;q$?t~0NGX9uY7dxolA`#AABmKPV8>4h>5v#CZ70u1U)-I?jW7|3X#_UZe;H= zVt^}sT7$+-KD1}Ith2n+bZN|#FcdF7pIEMl~(7n^%(!a@Jj_UTi!Acj=ASyinN-y3MS=-47MS zu;O7gKO5e=HVLuOTf4`(Xnx$r&F&E!jmp($J^{|E@=#qabBx49)pgzw|9T3Rn71)< zOkIL11EL|`lEVo5;AmRQiKrW9a2O?s!uVv6?nO5^sx59EY4D(~GeMO{?I02%K1Ti& zeMr<6c^aT`M035PI#Hj{Eqn9k?EzEKh=V@NPlvB$DT>Um2fqj45+tFJGdaQ#hEQNk zXh>(-7}VnX^MERi*>Xk%)Z3ez<^bU>!usVnBt7ZM;iDQ}N=o=#-pk?^ z8hdZy2r%l$V*>YKL&!c+rF36GhMgPP^G6UWknU{Dk9tcin&VZId$|829Hnr*a|FMW zf$3^|7|p{T(HF-}1C;MY8kz7d8~qvGII}-G@TAzRI9D0r5F8#fYW6EU=Vgo&7C^f} z#h?{inCa|$-xRvqBI)kEd#LCC))#|`6NE{Guq1IA)Ry$S8pRKzilDUv6hoGhz-J2A z)zn}DK_m_C7vk*~%^w=tP2w{&`_cP!?D7s?Qm;SyVu^FhafIE!<%*0_(e2Hn8cRXz zsoGDp9xHK&5L^STr~#k{^+#%2_2zbY3y#m*E{`rtUixep(7luW89l$M-bm^7!Tk1Z5qW|NY2-eGFkQEfwyA z&Crit8)hp86c{4Sf+!x9v2x6)rDl2*4dNbs%-0iIy762x>qr!}0!amz;;3}IR~p+M zbjbdMM5uv3_mnSfa1SCz3>{>-P{X=IZ9=1Ti$0{M=tX zll9LYe(HHfq3{^3Kgiq|pFtYE&-9O?xqG6SJVcmC6W)WGIh4rNu;}7FTTV2;xd9Jo zTo8p}XtwqMzFPRPpxOp>7Ow&Inj9ZrU3F^kvM?K-MFOHxO(I znwLIgdRh~0&Awsyi3Ay^xO0bYG=lLMak*}Q;M4L3Rp6aZ*3Nx%PsK3(ipN#-IFqHj8inzbFz*wk2 z@sEsK-+T~szX>xW&p$QYo_cS*NF|K?_B-;HatwG2CHXRXNBhTX=GN!c+ z^mZW`9c^K6vCz@}fmwv@-zH~i{Zc0+%K?KARo2j;_8CZCB1o2x7!(Ga#X~adMN+X> z4O)w}ze71jrZ5o;khZ}l3~%34;#P3sAy_wXkWL*qw{0f`quv(aAXt?o%=!brfYGi6 zi1z<nMk>@o1_iW))LX;z1D7gN26tf@y=0Hq-@GLi*MHpdgn> z))+uYR9ov0GAINdsfj~+7W1|Xa0uLyGAm#Owy3%M3J{lVGdZBSoj8Yj$zsHTI$4Ap$ z$6tJA^=jRY)Mou3{4n-Fpy{`VR^(zJ@<>V@)i*s zZkI31>v`tJ(DOSMC>gFw&;UQK9vcJ4oiE%fd?Ivc&vuu(ePzkNfjGj8%a|TmgMpil zrD7>x_(f&6AXvHzKsZ>InPo+%FH~>?lDRhca;P^ZdI`4gZm$N}@EGv25JdkZ$C1AE}_~3b+O=2B7og z41eG|-z3rVryzRD7bvYsv2b+4S=O8rMJrAQl67~a+|c4Sn4N!q_*v$qCF`EdJNM0_ zm-dw|kcuGQ2SLfFhKA`MFWb)FfjQERgRv`Ke|Rqi4D!n$SJ^@e7m+x#%1X~aBeFMr z#IOUJo8NZ*U4|h0V{(&1|0}yI1`DUj4OU~Ud%Ho@ z-yo4I_!XC+JA8&z#FSd1RuHybuFw(k9hCy=9Y0456MyDQ^wCxGK_C+lVO(mL-vUPv zLaFI|4TKxOxhHH>OqG3g;q1o|0f8P*$}*BMcrd^7dh#=VC)DTO=@8{z>Ci?Gy^0gq zDJ{B*$HGK`OA!e2dpocq&A`8@s_b~q4lehVrdK;RV87FY+o!=WdUA}mipr1cT8tCE z^3C_yNfb*^+614OObQIIQp=5{_h34HtfAzJhAo1+0QJzHvsqYggc0x;4s$eTNs9#P zg`*?unC_h@>YqT|QsJRFe0zA?4ur33=y;~N&qw@Bpdy_&{HKGV*;?mf3YW0=+_T|? zn{Jq;Q-7%o7noN_lx$~%VHO2c3u?}YBD#M9CZaTOM;yT(Ua%(kGwJzbzEbm`K=htU z1d^px?Bb5GuZ__#R1n zc3F4y%h4&xV&?KZy(PJH+vO0G;=aS{oIBwG`1s5{CXP7ua&ztP*SE{V-rs17y+SCB z9ogQcD#q3+RBtjRr{+TA|H@eUdE%?*Klr)c1?E?|>WqtOe;C^PchUFvyO)NR>*S2l zmxFWfqOj)}nvHsLdxl%=#LjOk4R6Zv^vN&PW^2_Ah&5~)DiNB*gVAyg#3?&h z08?cHy?v;-MQ+pYRmU&(PI81lnp;3?)!m>PMSqbDXGJ+dNlAO8jn{r%x5(<&%$!p07#nu+uQ+9N>hy*TOa#%(HSyW zYds;V9n$i`@jg_gqoqxp+vLkmZ7|*)k5$T_LL5tT(A0Sv{i@k)8Uk6*IZ)|Hg+VRS zWXrRhJ_g|BB?~9a;5e&gFJ|~{m-ww`d=&WgKmPankn=?H8Z`>O?z}GS9m?MWH|^O7 zaF2h9iHSXCYtQ-cWgW*101O$+iXe1s+1EdUJgVcCHSHAviN}+!>*nz}Y%U;C;vy2R zGzOkqB11I<#G?`O3psp|6Agkk@&&@w(_S7!PlXpVhp`mr58Hvl)*Jf+Aul zx=`+)K82q?QhBnIkTThEi^&r%f)^K?FQlB2%T=DqQvNkB48C#u z(o9TZ(9B|-)cX#p{QiX$jihuu-p1-5pz`=2<>bu~9j5B4(=ri}y`J9bG|W9OdqVXk z;}-jk@pjbhm&lC?(X_tF+i!L{PAL&@o+2 z)uFGnQzp9|K{|T!L`X2ke1@yIvAv8~AZJzx1hW=EKo1}>jF)|VeL|394*9F3%uJL4 zm5HHi$d)X$XuErH@tQ8C_XHf&R<8p1N?fzl4ZpBe=qiIe;1pz#k*%$gD%#q=8cRV= zlroI+8U8D?K9lmqRxa9g04}N@C11LiFL2ie)#(9@F$*;K3v(=Q~dX`9fg+2gV9Xwvc%_x>^_@GUMDl08mh zy`bfKL&L;gL=wyhUYUingyQ0V5MyexXhwhbb{B4?g`{kC7>|>Yqns3EQl2n>?}PQr z<~Mj)KEb@;gGXV;X!aG%7VwJAR+gwm)~?HWaWYVtRYM5EWDdD%{=8Fu=~Ds>7dVTl z5SPMyXavQ%J6$m2xIWu_q*926KbyR1J}Og*M&YVF6vtMAU`n=Y4bp^05nUwrjyFnt z5TPnPPJGC5F&lxWPbn;>dJV4cDOOt9lPr>u98{DPgbyIrC_OobiB%>UmfC1{iJ{Z8 zvuO}djw2%CON8|HCF)>i$+5IUKmUm(H5C(T39yL4YSb(Ydz0b4%ZZ9C^@@F)RJ&Yq z@0iewNz|`)1~6dX8^-flxM{H}m#>r-ADfn=MuHw66&2MY>bb=hUByZ`iiA**|2Qap zHK<8)3-N{<-)3s~`xW$ZkVss*0h&YD5U7Kt0c*zz3pM{;q$~jsi##K_kJQB9Cw|9o z7y_L`c|Padk95OICh5~;ietn5+^5TS{pL+L0wwytxr>r<^e_}s6Ui{rycim_nLri( zyiuzi)vb(spYiama(BY3Pn?Ex%D4U^KMn^}DqzM8~ynpz@^YHN>yeLCo?tO;s zVR9vX44|NNa&f0-HMULW-#Ms6OMz$m6!3HKQhrJ1cD5mX{fR6{GqfuLVNOE1F(-kbUIjkWvpa(Q8jD4Zb4a_ifLs%5<|(sx~@$qVd2H;(edo%AP9P zd5qohqtRV1jDjd@a2L18kXZj~H`a_Lmx(?-Gyqg2Y_|p*I zV=Nyi@%WdhsmlHjmVIF`QH$spmmtMENZ8KeWZOkegK0M|Z>ll}97O6Q@^r!-zym6^ zt>r|_t*La5j;fnkTJ~|1dal}Ysji+v-%0AH#!j9?_r!Ic+7Cn_@J|^Z-|6IAgBv^A zXHAxV=4mOh!_Ydk{4G=-jcxgCwL%glZ(Q$Ljm@MzCkZY@Z*@HOJstZ6&%`EV#hmQy zc+epi|8j?VybRguo}l<$wY>#nZEf6u`=z!dA9a*IaukyWk442NJ2%^i+#D)=%XODw zVD+i8@bgQ!yjd0G%x8s|XQ+Q`vXM}jMaqq15EJ8lbv2UT4~s9b+K14XH@0A6(ya|=*6^^+lC--XJRp$`)d;D%)HVEjS z@!+cN>}J`h{{C&+h1yFVExCy;RSANa9|oJSRSMhK+HSVvcEpS``~zEa$v30l(7)m< zyppjlA3@fTPmG@K489}UO~~lt!tRYHB!meWNIHIJ2o15{-$2@8t}~e;wjYBX_ZJ_# zXO$&h)|`c`c4fnBKrxJb?>jCure$8`nK&s*CUyrQe*Bm5v%~B_z+rE#oa)%*7WUO z;!s6_)mJ`z#2b#Hf8hX4_LH4KW^$0qw_aI!+Z%qVtrxC zk$u%qknT^rA>`Dd@DJU*|{1IXIFn^kgTbP2&F1l!d#Hw)|Pb(hEakLj zrsSCi3X(;6HXk+C@RCIQltlssg@g`YjQ(>S61{%?dJ7j`|7s8@X+j5Z+F;5bVDXgo zyFy6VAD4%VYE+9988~XxXsi-QD(5_UHAohiAtt6T-}s_rX7n2@C$A=8MX}du3~{oS z8>{vqQeMT}0KVC$@ZZsd;9xxr`4@NaWyK&kAz3bf{TS|`h64aX1Yt(ZOovWqEOq0^ zfnAO1MK%tJ##-Cf1UXYo6U7260c=_8-=S>;!FvS~38L3wXY&C6-yzghdNHEXGlAW8 zTsV3-R=sw?0J8#8RNgG{GW7eH(%BDhOUpG(^^cv*C^UA0h@CG^=oR5EnhEbV8XIYA zS%p?zCDQE48s2$4AMwh~AKbp4&cvPw%RDtT^`n6Z1$p(g>M_+X;Z4om??Rn0_YR_i zat!IJ(~fPD3qLxeVw5tdp<_O1guGbo@1GZ=&JX0!6dqr6`y{OOw^8+$WlmP5yGO`n zJis(t-(@Iax%QDTp=FIoNW+eBOFU7SF^>(}`t*`-1!a?jfRp_h05J0gJ_|=oliaod zIvRK{pa7Q8XF9!M)I)(B;k63Hk8*3s_E_a_`A>bSA=u&}V#I&3iE3w>5nqq%H;3rl zN)RIT)6&vLfz+5Rn6U*`6ysxvR*&Kg?^u;w(`h>$_ZBR9Jhz$ko5s;0)w!!0eHUIg z!BCEZG_sdBn6j%eZ7sYRQ&+QZQ7u~Y;D9944L;8NT0|W z>q+Zy2cLN)KW>0u2ACSp`pw#SxBL1=Qmiqb-FxMmb_@ICiPtU_Cn?PXj)yq*JGM(1LHk3mU$r2=F!QxUN z&vZ!gqFOcQ*H!CVyH8Zngqqr4v-YUuF7awCYA9cnXrOKDH2$rFH{Bs zgJNv1^;PuZ`J+M|7iARM6C(d{Z;5e!A^mPlYV!Hf6Jx@_TOAGqrJYJ=Av>OvO><7$ zi%`hhFx7ce$`BGG27p%cRip@Z3P}Vaqb|sKX!W&cw%Si*po!f&MM6rytPd`~W z(u4mUu$8Z?PKIu44^7yHHFIpZp=ZeR^TTwNR~3^%CU<$Ba>rzCR{#L6JZ27Dn7gf$ zQ#`z17kj*yPFDy=0V=AJ9TP0w5TmD{_z8a-T+zQbHfB*XLR5;+XNBNuy$O$utMT>? z9g*AeoT2XhD*Y*44MkH2kXSEu>Li}P z4kUu%37R?bc6S{C$RekvrCs&$@qs~W8uZ_Xj#n-ClzCEe?YUyJWJCl=`h~am#!tLU z^mivWV{8t0^$T7rJT8Ag>#8Lk{<#p3?UH`|H7BWG{uzzIm%!FPMKVu0wl3~+GM3UU ziWqfR-jddh`D5eO9;y7z_PLIJNT^H#|F0*+ikhcTp*{~^HK zA>rQe=;k6KgiGR+lhsO>GjQ!at1`k2nY^D16u$x20yL0B;n{wfP@@( zFP6{G&%MDZF9BnwzrVlL@KX&2h4|u!CER?|cm|cV4t^4^`fZ*MiLN)&U4Yv9wH3&h z0ByiSA`jlYd1GI;h9$Z+Qp29x!b&49&!9?5GB!E+0E$wulA!VV(;D^<*GZ!x_&-Hz z(}}`fe&4{{t0C*J1MqHQ)aGzXE-5QzP>nosvZL}S{4+0{c6U}CM{4bGJIi%tzKXtB zVri`2KLADvs}=-rKK7JX>L0VzyGXyIJeBzrg^d~w991O>Jh)b?{7QX(+o~XcSlf<4 zWqny`>{0$-h9msk%Qo;O+~jU?s|Q~9f`OCQ#puO1(iG+b*xyX}(-k87D;Tgz7LG{y zE6G-z8rogw+mFuqJdG2U3dw^^IGZlHz6{h<@s%8UeSAPN`5WVjxW0Z^SQw7dhcR1k z?{qkG1t6(+=gb8Pba-Ps{(uz^g8GPY9~fmC6o|JaZB>3Eu2+aXysgU=O z_@q}AJeVP#0?Xeq*z6v_le=Fa+`=yB@9iA4Y#Ws`+{NY1Y?hayahpE_Ei&3iVQg+i z_qti+IjCO^;dUhsh?19iTAL*12QP4H_J$a_{9g07p#?b6=2RWGAJKL=nebWgB>twu zv#Vq)Icq3r_=C?pPTinN5Oi?uTc1?Il3D=`Rj@hst*!DQ9TCbf zj=4Zi4ocNv?SAD*5!?k&vd@-HS-4sJ_E{^uzgg+&T$?{yW(Um$?0HiDc3&r@<2L7Y z9b_O#W3ETk%W#tc?d4itFRFp^N{k-JIs%( z>#DSsGy81Xb0{F@^^BZ|(C5z^dT%2P4B*vQN=>*?ZA)jB8V7`CCCB`7a;-aG)H>8c z&nX{XZ1<_9(z(nw{yNLBLzT;@;fE1(>LtsMzJD7RNB98uGmzwVvpnKt;Ui=txV?_G zDRl-6Xq>L++zH}jORDTTBtb|ixLrbTnG~iOE2kpt&ueQ; z9=}Th&I--?bV>~yjKqTB98Sz;+go^WG3%n?r+hE>ea$xg6ZecG!r;bI*nQK;Ghvh| zd-UMRzMtwknTG2XD=sFYh`V?9-Yp!RFP+#W%UHNK9gqmOxS?;GzQpY!VpN1!X!!us zLS?l`;_Kn;qygh|75<8W#|h}A#_Ch0OJ%rM6qjUb;?WHCE0201Dy>Cj`=q~P3%0ua z_5`yG)4*!$D^p)H+EU5FlHhyD&C(3(vL^_Nihf@!Lo)GKVhP24`Z6(X+apheYs{`BgkOs2wlf(W+@j@g-&; zsipvG%HoBFO73UhY=L;%!Y6g*WZ4ugnJ*_AK>wmSpCB-^%v7;OES)B*PeqvppI_i^ zqTN(EBdZ|}Yk;X`@%y)|x<>?I_edBtA7a;5jo4J8C4T08WWMOea%|ycHJKIJpK?P^ z>yP~5KYb>&S#=hLpr0-{IW<~p%Fd2twEbUSy<@O-DC11Op5hQb->Jd)RNJX&^IqbW zyG~sY+=M_;a+eDW3uTBhSdt6Bb4419)`iPVnK)}<0gRLx@U8gL)^U9qcfF^kodj)L zZU)(E@8PC^{hGEHN4{HwVmcqyx}z!Tj~zFqZ{iMkpFMe*C^`E;x4qJ&YteL5uy%8u zyLfopx-m*o1v8-xTvTb~NRr3$iFd1rjDDHH6yC@Cp9UhHIZWukThm;R>K7jspx-zVzba;_t zv2#}cT43{*K>WN=Ht(R~%});qlFxkZ29(pOlX=j69L1}3BMhpUQ24r!ZA~eyYyW}o z$;ZEX_FY;RusfdZrt_@}!dKB999NF}tYK_0%i7|&cs^s4ZYMoe zvR$r96qG-#_p;KI6T>7QZ{J$p#-&Ru`Xxw_U7tN`H-zp^#9QbCH{_Y1S>z|9ro&8I zp$&*qnM0P=_r=Ad8~j2-LbcsTKCRZ{#oc@V-~fg_zwoUwhk~*Nt}r?CPi>qoH;(un z3=`1&cAHk1-pmAu3Tn;Xv)z!|i;HR2u$pv$cIXOd9TPm@{vL*2u2dJRm)!l6Ovu~r z%f>h<{vwY9Ij0XdDA%$fooWm+N;}Oebotq&6?=s(8@2l<5%5V4A$yeyH22Ycm*7PKb&PRh)GF zCa3~&OV%7o0uTjec5ra8UR)dx-FVq*Y=I_g8;wKVH<%u=CTs%XDJl3ZLTO{NY=J14 z5ipiX(A~Wat;g>B`Cpofs1lT&btIa6h@;l zue;>cl)3h*Izxhkoj`%=f(c=X!1aqYNR_86mi!S5DJ-$fWZ2YVWa};ZNa9C=2er2Nw{XuXxZ6qyARlN`0s>Ci_7;*KvYqmu}Pt-O0_RAiB?=tZ$))53B;|d^^G$ zUqa4DAQM|b{q3oBnR4<#Gv=kTIRR(V;oinkoXE}ZWIGf(jA-OZlvjMT6^3NdhTI1V zDM>@eM8gIN0zWw%c1`A0o7NT5%A3^0@YXdcu6nULf6x09v*s*sx|6CbL6SRb2=%Jh z;w6mx#PH$_2Y`5Sf}NF~DA;7T-4PU>_Bfpm^p39p7vqh7(vKKyXZKFL;Z@q+A%CDO zA5fJ3!^+z9c@ti*XT$Ri*gk$rb*CtS8&3$+38y)8YBuwWFJev z7wR-8HnsD%T(Gq$#EFMGRWdN$h(PCu%>q6EY;N1J-!^Iy2t2MOueg{9l@6OQji~8B zSm;xS&zQ63g=rN0+y!O>quWvSg(UJ>NlpNXIjz}9`aXt9%YV-j}BH8>mY{QI*2AfR1OC?CgA2OFtB39jtfIx(j zk6G4N1oG;ybbm7_coI-k-Ptnlt#~)w1n|%dXqWbGH3jc~dg;RnB2jTrX_FHYg5Jlu z%m^5&hIkhZAal};4W#d_kPS={RJ*rfF;#d4J%eW;EW%)>-qDPUh$JE5*52yj)kkkk zi{~9TpD#}NQH$8Xs2?uXqpNmWG0$=ldU!o36<&X-G+1YJe9P6XlH zZY&?Z1?tv32%ZgcChujUf)H0B(KXl9#h@4ct&*Iif zka*?~%-#Pm$ywCU?#PH*Ymzkob?n~S%U;W)_o@0_PaAq&tI)XMbIM@w#OA(FcZQpC zaAPl!XZwi^nwuX$kLVvb3tvwB_+eWg_`QoEMS=I*3mJ%5Pi5zy;`k4)gQt;_P#qeK zE>1vsL^|o3Dk5A3u#We_iy#?kb&??Nr>*~f#{XqK14`gv=~CR=k;lGg&n#N>Ilr0m zMJB-37h9x+(Xz$Gdyu6Ye#3M~^aC)j3H|1~GerP*I}ce6rHeMtzgptJ#L!;ed_v4a zLVG(2MX-Jt?((f$PL5NjG>|XvbiU)J_s^@#wRV3MgE5Wkrd#W`@5W8B*H9D}Yq0ch zUYF*l7P2kRqZW~nNGCTElSuh8s*KfLttn+zb}Q%!G1=IVOf0`bzEwujk2>w|GO?u_ z@>ke)ws8p+2?<5$Nd5DzoCy5v&Ll^>3(Y$N%12vT+#GQ>NS{Vc5bZZI*b4S*i3BOD z-jVCHS_!o>Z%c*C8lak2ih@#0ko7Rl)Fb0T5xiI59~KG|gIh(j_KFuOhw3uUqQ z?AK=pi+s|A(bw;Ergx0TloolSE+TabOBqB@&tNP->nK;P8{xx01c>nK8Bovl%vr<2 z5;7c}q_?*8lO_%W2$)mPyxU4>F`}C5!PkXw`EYIG=)^`vN9)JEf15+SCUPU}4Pls1 zfSw9|5FD5P##7N|M!oz5iXKO6_mfC5cGOF}{n9k2iF4m0C%MHB1V|MX6(>tRg9XI? zdomBBHKfsMYZzF1-s(g!lpGGvWIu9Xl{d+qcfCGwU+Wh3qcJn_iqrS+Cab@N7D<#o zjAJt-TLs(T>jVi&rHk#I3;PI(i~dn9_Cp>}c6Xal04ryR-L?SedfBW#_>z6De><&se`dX`msRLNW=pg==9TKel{jVrNL;wKf-4O|pu(FbS z<{$T+l}H@jPLGz+SaX6?elIRVZUTRIlAAf}+iHUPRXk?T`eYFkrrcg! zyL^QCUJ)~c_klFr?eglD@W%s~hkm%GD`7C1)H!2hlLU=FQTon_{gEKdB=;QV$NW0;S^B=I+(q=4WO z7&IiDfHzv%(gDT@q{hOCj(2reeVb*#Gs6?ZH22JBOUs`S-iM`vNIq|QmX1A$=ghrw zf9q$X26cUq02VW-xR5L)K$^|~=w@&=0Pi^pr>j~2FhHUo@WQ3(pPl&(wy<6lcC3A{&_me1%4eU^ z?HnA`s<#?7+e%u~!lt9o2KY?QVOk<5td#Sr5ZGOCP`R2b)kz85{H zowXOQ-&eQ*@!(+qel-NqhAKu2?E!n|Qb7^|D#2msWuIkU6dZm3&Ico>c^f;x4{3_p zg<<-SXP17{5q&Z>oFE0Ee0hat6pl~Xm)Z~ii9jCC2Z&RUbg;&0!+#ir?94->mPQR?N{@&rfS zu1`sHQJ060HBPZl7;hd*Fv(Equ#9hkEjWn6)>92X(9ABYE?`8Hip^su^+b-aZU~~S zD{ZF*&VRnSDfAM$5T(es>DoYqbh!fx17pySOH%{`r>lWBl<=zM1A(6asg`l`eGD0b zS}lUw;z#A0a*JBS!WoKz3?&%|9q|S8JLC+(gbe1v%IT6ysHI4^&{LjtWj*>Rr~+5H z*~_K$s?&uO86!rK++quMaD^HAC?+}(hB-TQ6v%Fq0Xwe zGhE^|e={|f&hpEYk#_t6n#kuaHhMhjc73o{oY#eb1$cH(*B|?e?Ep z<*|v9x)fcgwgTN?F6cJy@roJr)ox{y1l-Z19sL@papLOoSCMOc5x=-t2_P}ZcJ_ya z-H>;n^E)#M&nXPgVe{tw43y8o0-!c*hOXnK&{r+;-b6GT6x~n72Clbe}%Wf?O)1bMBWOP&raFdEpD1066ye@cWO>e?l9Mx5}g| zWF=!F4bpA{VxjT+*fQK07$yzvsi|avDsvC#9nm%oJ-H)P9HK z&-RLdRNh43?FN|fB2Z4EPDUv(hC_sXD#`>r zr1`!*=xpevBqTNq#;hdy`K3WM@S|N47k`p}z&iWZ$9pnPdz z{O8sv-GwD|U6%fI#D?@t)w9 z6q@c6xHTz8ZexXC^{?)V$lrs}iI2HIyOb;8@rSX*e)I6#E@1bwg!?Kp$ZpQi_ zdo5p&Q9@UnbPX>E7|3ge(q8-4jm8;&EV}wo{sO)plDGi!bf8o(PeATP3gt435eaC# zB)HwQn4T{q9kn@1uS zG&8Ys>ePe0M-ajnf;coABJO(YOh^Q##K$`d7DJy4Dt*nqAx7L8c(0n6WcQYrjR-sLr+EJBwyq`%o>t2 zlT#)CzCKsk$)h!o%(Og@EC3WBTghfL>COdg;ieS9rh3W6Y`9>DSOot|K!*6MQ_w6! z(X0vYwx-_@X<(I+u9U&&y99yr>aQwbr$a&-48MyeCeXwC&Tl^;M;nW;uCDOl!Odh- zbtl-6o8K3E86-pN9VLP3tlL|VjwuclxR9{xo2ai-74HA-v*~b}4Eo+vnq6|kB$Bk3 zrrTKJHJ??P0h^Ij9IxMoDR##pyzEgbIadqWFuGqe0AMWL=r0H_e0YE@*g8Pz+ORBw z!HAtwHYWF;W*rQd|J!!oR1!#P;8irifhRA=sHvUA-_!S3VgU6wdszfNkzsTQIy}4q z4#j@3Pd$UJcZxq;gmEl$CsfMMpp>RA=(tPDrrf_^r3mBlW`r9dcC?L(AIP16zZxoe+iuRVIALvq< z9)BRGtgYQG2z`MnDr183*hM3bGz;)2X#TO$qp8Et0tmi_Hc`;t1fgUisN~z*(I6ic z*~+b8KR>?@H9leX@0T)ov86-XZV>qkLTl4)j-McJzO&e9Um;PON0wvCVVO_tcISbw z(zjk+0*vO#Z@moIjJVV|r9}=`Ypj?H)faWvC*L13q^ByWeHxcfYzc}7BlQkLg~8B@ zqc2E%}P{7;^KcP?&-2;Zs|iRIS5e2jU0$tG;gQJoE-=zM~M{)?-so zNI_K<%Qy_w41IIkj{)P;%}7Ec_WigR@4yVOMqERsZv(i3ZWg)Sa%60*ihUT*scu&z z-}H*ft%{m$=O?IblVMq+i}`;=T?sr?YuG>5#y;atC`&U#5+$T0)XW&N7DeGATe4-z z79z&jWl2(^>|5C`$y(Vf+4n6X*}_+fN?qS`y7zwH`TZ2LzUQ3hecxyK|Nl>imP>S8 z=xHMJoT$0eQ!@-e^&o}C!ospNJ)N-uPMt^c;wHw{LkG=A#c~#8r06r7p6zBneYs2;6A>ej%KPUwQzjC5hhdo=?`3%pR{2 zy#Y)_|H>?>+AJw)oFV8i35ACxCMMo^3#~-$uU}UMEc_w<{qj7_wcEA4hZ;XhM!;al zz#(iD%I?1Y;V{%O#X-rp>Pir}Fnd9e+`nV3EC?-$pjgaCnQou;I5ZJ}@R3fchg8cye-H3j zZO^VjP?Q9a2Y>}1E2ib$iRMD&6vmqMl0?k#^ycQ~Ft6kKu>C(&`DvW{ROa(=K2fZhIE_BbkJjI=H zn)MhkdyxIbHb7f7bjcT|-){s?*fZEPTEM-yi)s{{5YTStfer|@J5Kz3$Hxb#T_u5- z@8AG~38mOIglTz|*3K{wyTj_^s-viUo1LBgt`_eR(yL6iT|!BqwI`Xhf0YA(`ISk+ zLhqSK6K^82=QOMMzaqcbAoQ6=79LnZKngio0d3yEP;4xFTXl)<=T&}Wkde%M$;^ntL>XzChxOn{ z?82?0B3ML@h{ZpGXciP!F2#CuDGMfMW#&0fDC+llH<%ymd>9qYRg;PqQ~s86PI^z% z8_>9|gzLz;tWJO(!nHOb?wB;*U*@5D+EKp)GboS6!rOY$NP5LeI5qE z-o9P7UJzFW^u9O1PtrU?PTH9OTxDPV0gvA%nQX@cq=EavH9nIBto z75*TeGcteKd(Pfe;E$&A0S#5)>O+28VcPB5wQuoP2Qv($5|(S-u#u3Vw&lZ-v1jKQ zt)QF=HF(IbecjxwfLg+xONr2Hvg6>>h0o-5h15CD>24XC&z^IRqwIN0`?Do^_%ou7l1Is}40^*8aYe0o#SGXbfzHz9~{{7kCa5W1Ykq$Ll^haX& zS?p^u&FV;mUdGdgBHQ6a`+*uW3O|b=VcC7m{k^#Y2IO+U$dlK!U&Iz4l~JP#(FgdAVI#}5%?>Rbq3Wm zm8ppMwoiJ|9Go9_98>gSA$&39Sy1wgBb=QVDE2JQ&}gW}UjsOlec)I+*w|!Au_lUe zj_I*(Xybs5EiNv8kD5uwDFIDU-JOL}YIMd(rtf|FHXwf?ADbIVqkb2ASMmLzVMNLs zmY#!=o_i0eodkfZ_*V@HRuZiI0Ea^vM%x9BTb6NX3%0G4oO^veJv|ShB|T0s@f4eN zi-rVT8D8r@5`2Aty^LNO8008GcO2Z^6VSF+Sy=GMFFgCa(+yxr;7q*QCn@>TBP|Qt zB8;uPhslB5qn(|!!-EhiDU-cg?3kc=MLP+$!2;6Qw#qMhZT5Qu-B5A?*i(eZSLwfFk-pj1Xz^ zS$iuQ8y~*~{Id`*?!x2kZ=CZYoj2B>)Ko|GV+xW)TNXOSp)(ye@_Muy?XLs6I9VIV z)WsmUu91=9;o(&rf{7iT^&&(bIyo*K=!-Dpe`X`c80Rby0Dgk^Q3-)Jn#8CBBw0+! zG)hL|z-a-$`vU~*phr3EUA_-9Mq$<2C&z`uych_eC!4EBSqs5D82}~Z-9Y0i^8+T@ zue~^V4Z{&MMKcfNAvRwqwZnELD!|EB%{b^^vgpG~#8cvP?%nak?eB}KejFQ9eKQi+ z3Xr4+gzsUHpa6`cY)Z4#{$N7dQYUN~j&Soq#6o*=UDaF3PHUQkJ z9yH&T4uN$2os4?`{(@X`{%(#i0;}2sdmYG5D-=jBk%B;D>!P#|wnnw&SLf8v&WDnc zlAx+ke@EC95zlV8#zdiDd!l#B)T5^T1Cq##^Xq=8ckfEo(uMfg4jTxaf#r-tUJDZb zP?@7?%f<5GFvyd~^=AEw_NkJ#v2d;K<~izMSzCl`0adTxZ<0%2KD|fuiakLuyPK_X z2gWwfP|(owu)7D#y%bN}$^QxAbJ^LxpG4Zo-t)^LN>2Hj7Y`?bY2t5^ljvRtC+)0p zGH7Vf;^j4=D~ii_+chbN8g%2mWUU(2^o{n#o{dI828>P>-%6NCOIq6kOXyODVmf!= z6u@cK+k{?`cbS`Xu38c;*@6}O8yxeJ&1n|HQSxnZz`QDk@Hl2sNu(NKeP=}P*$liY z5&@bi#>UyAas`RV;hBFo(1!_vxhOgDAlEFBnW^BAb>)yJ14V92Qu0uS1Uuz1MeZzI4>6P*D~eAf@|5r+Jx4X=f^CI zjr?epw<4uih|sAVAi(S2uUCV*!wTHEg7Jlba&>0=)Qz-3PPH$5%!vJ`+M7{vT9UQG zup(J3KW-wjTG_WzAz-KgM@2_6=|G}Z4=h}yA9v3xLEwjP`(t%1WYinzkNdj(7W~AZ zLu+Q*ZVq}dFW|hY>W?k3r(jnJh*edIJY=M!XjFi9N0%%^UsTs|ABS-tp>X0_n=>zl zikK-0=z#6X3xIP8Fq1Pcq3OIjIef_SLhud}T&!si)ow$6V zyvO0Q6oMFp?(_nY-=FIsOf~W+Ds40mpn6O)6Eg6Tz3&-O{_26-kj=EQk(zihX1Qgj z@Cwbsqn&+qj{(y_6V1bKKut0XlH~@S$@-7hmb8&Amvj;=SuQv_8oH@m!%2 zKOKvZi|f9d`$Uu(NLkt8kw|j3i7=60J=@hP_N@^Axd|@3ZKnBvCK+?~m+uN%9~l-4 z{L-rd6@xzK2lLjq`Ff1Hrj4HGE6CauXO*R^n~x3deZ1+XBNl3N1tH7)cv*wLk-I;8 z(!YU;f+6#eAYw2TIRn@U4kbB({R!O~ljM6s(d9iKL~?+x@brp`1nD?PW?LqzFed%m zdB5ZYfA#lx&Z`oJAWo$x&~DIc$uv)}A17?colEllihzw%Y!iy94Nw@oE^A$(7Lg z%|c}1NW}f&p#VJ@SopO{*=Vyzo2;p>?=yInel8*T$P86W$xir zk`s$RjN=7-UmE1D8l%mP?(7T!FyP28j1t_Gku$7_LH}`IQ-C9Byw08Rvve3gT5&}M z(~MPFM@MqLI)o(B7R_jWANDO2!x~(ho%U>~tI0p@nSE`-itP06?>|U`IR7Ji#*1gO zqO-~rY(uopXsm<)uxX&Nk+K59a3?=QabtUm_C6;4JA5QMEOVuvSj)@c=~u_iz6>OT z?|=;~leCR!n?C>p&<_!_dU;fWh`0aIr(6Mb0XYG^3zO3*K|JY@M9CKScJ2$JuEOWSk z;F6c&5_T+oQMOTM&$-Trr_!5Tv5cakzw8tW0Z2Z44LF<^+`B-A_&GQN*WID)6$X&P z9TbETdD}22miZV18IWQvEG>;s=A@9a0BJk^Z1_9R# zOrBYL=*U3@t^fdbAdE!)FyNwpupwQJ*$ZmV@6;$WoT0qVAI3Wk>F8& z>rMfWgV&zjS)HSB_ZiuSsB$+0;GfC|{cvHa!B7xnA-bjK{JWR1~2CCI&FT4Ct;a)Elp|{^l?jKoGH^aEeHz zj#GD{1!?AM+`XB6Xy-=2P~xKiH~=QuH$6=NA~NqJFbFgOqpzG_(RKV$Lihaslnz6{ z!U5?`v^@mQij1$y)HSJnJ{oIe7^wlLLtwRlawb$M98)#(9Q#7~P895*yvV^q@)xMB z%5lx2?Zjbk5DSD0NZz?S^ie-Yl79=|2J(n2xA2qq%+8>j+y?X;v3Pv7@cuc;W zLtcWTjhg!Skb$B)_Wb^hyz6>itUF5%AakvNpM?a4eQKUfY95vLzSdRh?^z1gJ(3C* zS9uU%)0qZ@E@kNYNFo6fP_;W$7=t*xC+#n2}~*Eh+0kE-IH!a3(hUpzV|PBDa7 zgQZKRgu)vkWpos_d;qZ)(rD0>0|dUFo>W#|m`Td8@F4bWmYtu*@|W~AW5ZA-8qu)Z(m8E^C=)oN~xILp36 z+2YG+%~&X=bpRHW7906NIc|;q%DlQzcTT3e#|zcC)ur*Apwr3KOQP#OZu+D~R8tpjPmZvX+m72r}em0l0qYjanO->8?BlS}RUb>cswlAa3m6 z6mq>Y5ZJr1iEDnA3&0OxwJwg^OA|*1#YLOEj<3{}$b=DVN;;rXeJ%{nA3be@Qg5v0 z_T$}+=S$0+!wDZ5mQ)hSV)z%FG+@szY@y8@>>ycUh!28(EI-%4r};kWa#Tu?_q%hZud8>$1d^Wi5ALeL`Q>hsOB5=FhHwuTU5Yl9o3uUV!DKGdr79 zoDPy9mQr}2=Br|uBPn2o@P3gdGm-kE?i}|&t9E*gWU+lJD*6#_{Zf?r&kFe~{8Odb z>$zM4EdyluV5fw)c*hi8%_?^iW#268qhKogwE`>)ME%Dmgdhdu)zS#ey}%=KaE9nt5S8%*~H{YE~n zw)k(XORMFj&P#8~52`-=d1Qlw2?@qZv%u@IbigljEDQblb)0x`Cp-ZABZBmJQeCSa zJc{-(w#-j>--&+!kNvi<;5@D@foe}|!{Es{bqRFza1^x3YvfLpUbc--NUdi%meg$= z6JyeCo4x1UqB5ZkzH0jSc^3EFJov+8iQU&tQbuV^Sp)qxIC{c(-+A2_e%bC5s%x@5 zf46Jp{*p)MkKOPTRs^;D3@#DvRQ(ip$w|uhBhw(C{m)Pkr`b=h%Wx@Wz3nMD51mIp zgfCw;x@}(&fP<^>HRC7x@e2((2Dy}{akI&=#G;2m{zvRt(Yg@*k-bJ{p8~r+c)n^M zsz@?jl~41`HRc7M8EJZn{)^aOC_CInt>;I7hov>{DyERjqRC(6na;!Ksq=T2rt%X@ z+2EN*BKpxU-c);kRTY4U<@b>>NQy|_`s%euMYv?brIwMrvAO$u+LMhF=dW_TLodcZ z?<)Apalih&p_H_QP~$Ea#|DTIejkeOv1gafsY4%2J@d?)G&iK3CvlI53>hjg7g>Ls z8KFAqCVr|y(wqHiPV|4bK~RLcB$iUXk(?L%9&&FG2Ie@}mdh~wTF^734# z;L@Up+$^u@(^|$&RX6cc?IVjN5X=7e(%=oxiQTwR_wWC_5)qxl`ef^laQl{lvdjmM zxfAT@HTw6)?lQw1V%q5|o+!=v&&wC~MwAtue6&%X4y5)}>=?K2lfGH$8ag`qf2~Nd zF3wCmtIV`Z$Sps;`JR?lSjF!Z3PBw1scRjc=dbz_P%vNK`R!SLK%CRXb)DyKL(6bs z|9yMIf$&|n7Tf=K-g|k8-etNbYnzsx^F{MD**7jYKK))znOm2^ZFADO=}_^Zp-iW5~R{Eoaw88z~BZO*3 zr!R-_uAS)|pE%^FunyMee needs camera access to take photos for generation. NSPhotoLibraryUsageDescription FunyMee needs photo library access to choose images for generation. + NSPhotoLibraryAddUsageDescription + FunyMee saves your generated image or video to your photo library when you tap Download. diff --git a/lib/core/auth/auth_service.dart b/lib/core/auth/auth_service.dart index a23e8ef..9ce3658 100644 --- a/lib/core/auth/auth_service.dart +++ b/lib/core/auth/auth_service.dart @@ -87,4 +87,37 @@ class AuthService { /// 登录流程是否已结束(含 fast_login + common_info 链路);用于遮罩,勿用 [loginComplete] 的 Future(首帧可能为 null)。 static ValueNotifier get isLoginComplete => FrameworkAuthService.isLoginComplete; + + /// 在 **登录成功**([UserState.userId] 非空)且 [isLoginComplete] 为 `true` 之后执行 [onReady]。 + /// + /// 若登录流程已结束但无用户(失败),调用一次 [onFailed]。若 [isLoginComplete] 已为 `true`,同步执行。 + /// + /// 返回在 [State.dispose] 中调用的取消函数,避免界面已销毁仍监听。 + static VoidCallback whenLoginSucceeded({ + required VoidCallback onReady, + VoidCallback? onFailed, + }) { + void runOnce() { + final uid = UserState.userId.value; + if (uid != null && uid.isNotEmpty) { + onReady(); + } else { + onFailed?.call(); + } + } + + void listener() { + if (!isLoginComplete.value) return; + isLoginComplete.removeListener(listener); + runOnce(); + } + + if (isLoginComplete.value) { + runOnce(); + return () {}; + } + + isLoginComplete.addListener(listener); + return () => isLoginComplete.removeListener(listener); + } } diff --git a/lib/design/pencil_theme.dart b/lib/design/pencil_theme.dart index 937e8fd..fd0a01d 100644 --- a/lib/design/pencil_theme.dart +++ b/lib/design/pencil_theme.dart @@ -51,6 +51,16 @@ abstract final class PencilTheme { static const Color genSlotBorder = Color(0xFFF5D08A); static const Color genNavBackStroke = Color(0xFFE7E5E4); + /// Credit Record 流水行卡片(`funymee_home.pen` listCr / ez9wP) + static const LinearGradient creditRecordRowGradient = LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Color(0xFFFDE047), + Color(0xFFF59E0B), + ], + ); + /// 设计宽度用于按比例缩放(可选)。 static const double designWidth = 390; } diff --git a/lib/features/generate/generate_progress_screen.dart b/lib/features/generate/generate_progress_screen.dart index 3abc4d8..01efe5a 100644 --- a/lib/features/generate/generate_progress_screen.dart +++ b/lib/features/generate/generate_progress_screen.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:client_proxy_framework/client_proxy_framework.dart'; @@ -27,29 +26,38 @@ class GenerateProgressScreen extends StatefulWidget { } class _GenerateProgressScreenState extends State { - Timer? _timer; + ImageProgressPollHandle? _pollHandle; String _status = ''; int _progress = 0; String? _resultUrl; String? _error; - bool _finished = false; + bool _navigated = false; @override void initState() { super.initState(); - _poll(); - _timer = Timer.periodic(const Duration(seconds: 2), (_) => _poll()); - } - - Future _poll() async { - if (_error != null || _finished) return; - final uid = UserState.userId.value; - final res = await ImageApi.getProgress( + _pollHandle = ImageProgressPoll.start( app: currentBackendAppType(), taskId: widget.taskId, - userId: uid, + userId: UserState.userId.value, + interval: const Duration(seconds: 5), + onTick: _onProgressTick, + onTransientNetworkFailure: (n, max) { + if (!mounted || _navigated) return; + setState(() { + _status = 'Reconnecting… ($n/$max)'; + }); + }, + onFatalError: (msg) { + if (!mounted || _navigated) return; + setState(() => _error = msg); + }, ); - if (!mounted) return; + } + + void _onProgressTick(ProgressPollTick tick) { + if (!mounted || _navigated) return; + final res = tick.response; if (!res.isSuccess || res.data == null) { setState(() => _error = res.msg.isNotEmpty ? res.msg : 'Progress error'); return; @@ -61,59 +69,31 @@ class _GenerateProgressScreenState extends State { _resultUrl = p.resultUrl; }); - if (_isTerminal(_status) || _hasUsableResult(_resultUrl)) { - _timer?.cancel(); - if (_isSuccess(_status) || _hasUsableResult(_resultUrl)) { - _finished = true; - if (!mounted) return; - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => GenerateResultScreen( - taskId: widget.taskId, - resultUrl: _resultUrl ?? '', - ), + final doneSuccess = ProgressPollSemantics.isSuccessTerminal(p.status) || + ProgressPollSemantics.hasUsableResultUrl(p.resultUrl); + if (doneSuccess) { + _navigated = true; + _pollHandle?.cancel(); + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => GenerateResultScreen( + taskId: widget.taskId, + resultUrl: _resultUrl ?? '', ), - ); - } else if (_isFailure(_status)) { - setState(() => _error ??= 'Task failed ($_status)'); - } + ), + ); + return; } - } - bool _hasUsableResult(String? url) { - if (url == null || url.isEmpty) return false; - return url.startsWith('http://') || url.startsWith('https://'); - } - - bool _isTerminal(String s) { - final t = s.toLowerCase(); - return t == 'success' || - t == 'completed' || - t == 'complete' || - t == 'failed' || - t == 'failure' || - t == 'error' || - t == 'cancelled' || - t == 'canceled'; - } - - bool _isSuccess(String s) { - final t = s.toLowerCase(); - return t == 'success' || t == 'completed' || t == 'complete'; - } - - bool _isFailure(String s) { - final t = s.toLowerCase(); - return t == 'failed' || - t == 'failure' || - t == 'error' || - t == 'cancelled' || - t == 'canceled'; + if (ProgressPollSemantics.isTerminalStatus(p.status) && + ProgressPollSemantics.isFailureTerminal(p.status)) { + setState(() => _error ??= 'Task failed (${p.status})'); + } } @override void dispose() { - _timer?.cancel(); + _pollHandle?.cancel(); super.dispose(); } diff --git a/lib/features/generate/generate_result_screen.dart b/lib/features/generate/generate_result_screen.dart index 076ed65..085f361 100644 --- a/lib/features/generate/generate_result_screen.dart +++ b/lib/features/generate/generate_result_screen.dart @@ -1,12 +1,23 @@ +import 'dart:io'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:gal/gal.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'package:video_player/video_player.dart'; import '../../design/pencil_theme.dart'; -import '../../widgets/pencil_chrome.dart'; import '../report/report_screen.dart'; -class GenerateResultScreen extends StatelessWidget { +/// Generate result: full-bleed media (clipped), top nav, bottom save + report (design `2SyyL`). +/// +/// **Why match [HomeScreen]’s Scaffold:** with `extendBody: true`, Material +/// wraps the body and adjusts [MediaQueryData.padding] for the subtree (see +/// `scaffold.dart` `_BodyBuilder`), unlike the default used on the home page. +/// That diverged from full-bleed behavior; this screen keeps `extendBody` false. +class GenerateResultScreen extends StatefulWidget { const GenerateResultScreen({ super.key, required this.taskId, @@ -16,94 +27,489 @@ class GenerateResultScreen extends StatelessWidget { final String taskId; final String resultUrl; - bool get _hasUrl => - resultUrl.startsWith('http://') || resultUrl.startsWith('https://'); + @override + State createState() => _GenerateResultScreenState(); +} + +class _GenerateResultScreenState extends State { + VideoPlayerController? _video; + bool _videoInitFailed = false; + bool _saving = false; + + bool get _hasUrl { + final u = widget.resultUrl.trim(); + return u.startsWith('http://') || u.startsWith('https://'); + } + + bool get _isVideo => _urlLooksLikeVideo(widget.resultUrl); + + @override + void initState() { + super.initState(); + if (_hasUrl && _isVideo) { + _initVideo(); + } + } + + Future _initVideo() async { + final uri = Uri.tryParse(widget.resultUrl.trim()); + if (uri == null) { + setState(() => _videoInitFailed = true); + return; + } + final c = VideoPlayerController.networkUrl(uri) + ..setLooping(true) + ..setVolume(1); + try { + await c.initialize(); + await c.play(); + if (!mounted) { + await c.dispose(); + return; + } + setState(() => _video = c); + } catch (_) { + await c.dispose(); + if (mounted) setState(() => _videoInitFailed = true); + } + } + + @override + void dispose() { + _video?.dispose(); + super.dispose(); + } + + Future _saveToGallery() async { + if (!_hasUrl || _saving) return; + setState(() => _saving = true); + try { + final ok = await Gal.hasAccess(); + if (!ok) { + await Gal.requestAccess(); + } + final uri = Uri.parse(widget.resultUrl.trim()); + final res = await http.get(uri); + if (res.statusCode < 200 || res.statusCode >= 300) { + throw HttpException('HTTP ${res.statusCode}'); + } + final ext = _isVideo ? _guessVideoExt(widget.resultUrl) : '.jpg'; + final file = File( + '${Directory.systemTemp.path}/funymee_${widget.taskId}$ext', + ); + await file.writeAsBytes(res.bodyBytes); + if (_isVideo) { + await Gal.putVideo(file.path); + } else { + await Gal.putImage(file.path); + } + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Saved to Photos', + style: GoogleFonts.inter(fontWeight: FontWeight.w600), + ), + ), + ); + } on GalException catch (e) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.type.message))); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Save failed: $e'))); + } finally { + if (mounted) setState(() => _saving = false); + } + } + + void _openReport() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ReportScreen(taskId: widget.taskId), + ), + ); + } @override Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - gradient: PencilTheme.yellowWhitePageGradient, + final bottomInset = MediaQuery.paddingOf(context).bottom; + + return AnnotatedRegion( + value: const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + systemNavigationBarColor: Colors.black, + systemNavigationBarIconBrightness: Brightness.light, ), child: Scaffold( - backgroundColor: Colors.transparent, - body: SafeArea( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(2, 0, 14, 10), - child: SizedBox( - height: 56, - child: Row( - children: [ - PencilRoundBackButton( - onPressed: () { - Navigator.of(context) - .popUntil((r) => r.isFirst); - }, + backgroundColor: Colors.black, + resizeToAvoidBottomInset: false, + body: Stack( + fit: StackFit.expand, + clipBehavior: Clip.hardEdge, + children: [ + Positioned.fill( + child: MediaQuery.removeViewPadding( + context: context, + removeTop: true, + removeBottom: true, + child: MediaQuery.removePadding( + context: context, + removeTop: true, + removeBottom: true, + child: ClipRect(child: _buildBackdrop()), + ), + ), + ), + Positioned( + left: 0, + right: 0, + top: 0, + height: 140, + child: MediaQuery.removeViewPadding( + context: context, + removeTop: true, + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.55), + Colors.black.withValues(alpha: 0), + ], ), - Expanded( - child: Center( - child: Text( - 'Done', - style: GoogleFonts.inter( - fontSize: 19, - fontWeight: FontWeight.w700, - fontStyle: FontStyle.italic, - color: PencilTheme.stone900, - ), - ), - ), - ), - const SizedBox(width: 44), - ], + ), ), ), ), - Expanded( - child: ListView( - padding: const EdgeInsets.all(20), - children: [ - if (_hasUrl) - ClipRRect( - borderRadius: BorderRadius.circular(16), - child: AspectRatio( - aspectRatio: 3 / 4, - child: CachedNetworkImage( - imageUrl: resultUrl, - fit: BoxFit.cover, - progressIndicatorBuilder: (_, _, _) => const Center( - child: CircularProgressIndicator(), + ), + Positioned( + left: 0, + right: 0, + top: 0, + child: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(2, 0, 14, 0), + child: SizedBox( + height: 56, + child: Row( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => Navigator.of(context).pop(), + borderRadius: BorderRadius.circular(14), + child: const SizedBox( + width: 44, + height: 44, + child: Icon( + Icons.chevron_left_rounded, + size: 26, + color: Colors.white, + shadows: [ + Shadow( + blurRadius: 8, + color: Color(0x99000000), + offset: Offset(0, 1), + ), + ], + ), ), - errorWidget: (_, _, _) => - const Icon(Icons.broken_image), ), ), - ) - else - Text( - 'The result is not ready yet. Check History later.\nTask: $taskId', - style: GoogleFonts.inter(color: PencilTheme.stone600), - ), - const SizedBox(height: 24), - OutlinedButton.icon( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ReportScreen(taskId: taskId), + Expanded( + child: Center( + child: Text( + 'Save', + style: GoogleFonts.inter( + fontSize: 19, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + color: Colors.white, + shadows: const [ + Shadow( + blurRadius: 8, + color: Color(0x66000000), + offset: Offset(0, 1), + ), + ], + ), + ), ), - ); - }, - icon: const Icon(Icons.flag_outlined), - label: const Text('Report / feedback'), + ), + const SizedBox(width: 44), + ], ), - ], + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: _ResultBottomBar( + bottomInset: bottomInset, + saving: _saving, + onSave: _saveToGallery, + onReport: _openReport, + ), + ), + ], + ), + ), + ); + } + + Widget _buildBackdrop() { + if (!_hasUrl) { + return ColoredBox( + color: PencilTheme.stone900, + child: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + 'The result is not ready yet. Check History later.\nTask: ${widget.taskId}', + textAlign: TextAlign.center, + style: GoogleFonts.inter( + color: Colors.white70, + fontSize: 15, + height: 1.4, + ), + ), + ), + ), + ); + } + if (_isVideo) { + if (_videoInitFailed) { + return ColoredBox( + color: PencilTheme.stone900, + child: Center( + child: Icon( + Icons.videocam_off_outlined, + size: 48, + color: Colors.white54, + ), + ), + ); + } + if (_video == null) { + return const ColoredBox( + color: Colors.black, + child: Center( + child: CircularProgressIndicator(color: Colors.white54), + ), + ); + } + final c = _video!; + if (!c.value.isInitialized) { + return const ColoredBox( + color: Colors.black, + child: Center( + child: CircularProgressIndicator(color: Colors.white54), + ), + ); + } + // Match [home_screen] `_HomeItemVideoBackground`: viewport cw×ch, FittedBox.cover, clip overflow. + return LayoutBuilder( + builder: (context, constraints) { + final cw = constraints.maxWidth; + final ch = constraints.maxHeight; + final w = c.value.size.width; + final h = c.value.size.height; + if (w <= 0 || + h <= 0 || + !cw.isFinite || + !ch.isFinite || + cw <= 0 || + ch <= 0) { + return const ColoredBox( + color: Colors.black, + child: Center( + child: CircularProgressIndicator(color: Colors.white54), + ), + ); + } + return Stack( + fit: StackFit.expand, + children: [ + Positioned.fill( + child: ClipRect( + child: SizedBox( + width: cw, + height: ch, + child: FittedBox( + fit: BoxFit.cover, + alignment: Alignment.center, + clipBehavior: Clip.none, + child: SizedBox( + width: w, + height: h, + child: VideoPlayer(c), + ), + ), + ), ), ), ], + ); + }, + ); + } + return SizedBox.expand( + child: CachedNetworkImage( + imageUrl: widget.resultUrl.trim(), + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + progressIndicatorBuilder: (_, _, _) => const ColoredBox( + color: Colors.black, + child: Center( + child: CircularProgressIndicator(color: Colors.white54), + ), + ), + errorWidget: (_, _, _) => ColoredBox( + color: PencilTheme.stone900, + child: Center( + child: Icon(Icons.broken_image, size: 48, color: Colors.white54), ), ), ), ); } } + +/// Bottom actions: gold CTA + hint + report on transparent background (no white bar). +class _ResultBottomBar extends StatelessWidget { + const _ResultBottomBar({ + required this.bottomInset, + required this.saving, + required this.onSave, + required this.onReport, + }); + + final double bottomInset; + final bool saving; + final VoidCallback onSave; + final VoidCallback onReport; + + static const _goldGradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFEAB308), Color(0xFFCA8A04)], + ); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(20, 16, 20, 34 + bottomInset), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: saving ? null : onSave, + borderRadius: BorderRadius.circular(999), + child: Ink( + height: 54, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999), + gradient: _goldGradient, + boxShadow: const [ + BoxShadow( + color: Color(0x40B45309), + blurRadius: 16, + offset: Offset(0, 6), + ), + ], + ), + child: Center( + child: saving + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + 'Save to Photos', + style: GoogleFonts.inter( + fontSize: 17, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + ), + ), + ), + const SizedBox(height: 12), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onReport, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.flag_outlined, + size: 18, + color: Colors.white.withValues(alpha: 0.88), + ), + const SizedBox(width: 6), + Text( + 'Report', + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white.withValues(alpha: 0.88), + shadows: const [ + Shadow( + blurRadius: 6, + color: Color(0x80000000), + offset: Offset(0, 1), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +bool _urlLooksLikeVideo(String url) { + if (url.isEmpty) return false; + final lower = url.toLowerCase(); + const hints = ['.mp4', '.m3u8', '.webm', '.mov', '.mkv', '.avi']; + return hints.any((h) => lower.contains(h)); +} + +String _guessVideoExt(String url) { + final lower = url.toLowerCase(); + if (lower.contains('.webm')) return '.webm'; + if (lower.contains('.mov')) return '.mov'; + if (lower.contains('.m3u8')) return '.mp4'; + return '.mp4'; +} diff --git a/lib/features/generate/generate_screen.dart b/lib/features/generate/generate_screen.dart index 497d40c..a6fa428 100644 --- a/lib/features/generate/generate_screen.dart +++ b/lib/features/generate/generate_screen.dart @@ -1,10 +1,13 @@ +import 'dart:async'; import 'dart:io'; +import 'dart:math' show max; import 'package:cached_network_image/cached_network_image.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:video_player/video_player.dart'; import '../../core/app_env.dart'; import '../../core/user/user_state.dart'; @@ -28,9 +31,13 @@ class _GenerateScreenState extends State { final _picker = ImagePicker(); File? _picked; File? _picked2; - String _heatmap = '720p'; + /// 对应 create-task 逻辑字段 `size`(换皮 `seminar`),如 480p / 720p。 + String _outputSize = '720p'; bool _busy = false; + VideoPlayerController? _previewVideo; + bool _previewVideoFailed = false; + static const double _slotW = 112; static const double _slotH = 108; static const double _previewH = 359; @@ -42,6 +49,35 @@ class _GenerateScreenState extends State { /// 双图:仅当扩展配置 `img_need == 2`(与 [ExtConfigItem.imgNeed] 一致)。 bool get _needTwoImages => widget.template?.imgNeed == 2; + /// 与 [app_client] `ImageApi.createTask` 的 `congregation` 一致:值为 [ExtConfigItem.templateName], + /// 缺省(纯本地 extConfig)时用 [ExtConfigItem.title];`BananaTask` 不传。 + String? get _templateNameForCreateTask { + final tpl = widget.template; + if (tpl == null) return null; + final tn = tpl.templateName?.trim(); + final raw = (tn != null && tn.isNotEmpty ? tn : tpl.title.trim()); + if (raw.isEmpty || raw == 'BananaTask') return null; + return raw; + } + + /// 任务类型:FunyMee 文档/换皮为逻辑 `taskType`(V2 `liaison`);优先 [ExtConfigItem.taskType],再 [params]/[detail]。 + String? get _taskTypeForCreateTask { + final tt = widget.template?.taskType?.trim(); + if (tt != null && tt.isNotEmpty) return tt; + final p = widget.template?.params?.trim(); + if (p != null && p.isNotEmpty) return p; + final d = widget.template?.detail?.trim(); + return (d != null && d.isNotEmpty) ? d : null; + } + + /// 对应 `ext`(profile):仅当 params 与 detail 同时存在时传 detail,避免与 taskType 重复。 + String? get _extForCreateTask { + final p = widget.template?.params?.trim(); + final d = widget.template?.detail?.trim(); + if (p != null && p.isNotEmpty && d != null && d.isNotEmpty) return d; + return null; + } + Future _pickSlot(int slot) async { if (!mounted) return; final source = await _showPickImageSourceSheet(context); @@ -174,8 +210,10 @@ class _GenerateScreenState extends State { sourceFile1: _picked!, sourceFile2: _picked2!, userId: uid, - heatmap: _heatmap, - cipher: '', + size: _outputSize, + taskType: _taskTypeForCreateTask, + templateName: _templateNameForCreateTask, + ext: _extForCreateTask, compressFirst: true, compressOptions: const CompressImageForUploadOptions( maxSide: 1024, @@ -187,8 +225,10 @@ class _GenerateScreenState extends State { result = await ImagePresignedUploadCreateTaskFlow.run( sourceFile: _primaryFile!, userId: uid, - heatmap: _heatmap, - cipher: '', + size: _outputSize, + taskType: _taskTypeForCreateTask, + templateName: _templateNameForCreateTask, + ext: _extForCreateTask, compressFirst: true, compressOptions: const CompressImageForUploadOptions( maxSide: 1024, @@ -231,9 +271,105 @@ class _GenerateScreenState extends State { } } + /// 与当前 [_outputSize] 一致;优先 [ExtConfigItem.cost480p] / [ExtConfigItem.cost720p], + /// 否则用 [ExtConfigItem.cost] 作 720p,480p 按半档估算。 + /// 与 [HomeScreen] `_HomeItemVideoBackground`:优先 [ExtConfigItem.videoUrl],否则用 `image`。 + String? _previewPlayUrl(ExtConfigItem? t) { + if (t == null) return null; + final v = t.videoUrl?.trim(); + if (v != null && v.isNotEmpty) return v; + final u = t.image.trim(); + return u.isEmpty ? null : u; + } + + bool get _previewIsVideo { + final u = _previewPlayUrl(widget.template); + return u != null && _urlLooksLikeVideo(u); + } + + Future _pauseThenDispose(VideoPlayerController c) async { + try { + await c.pause(); + } catch (_) {} + try { + await c.dispose(); + } catch (_) {} + } + + void _disposePreviewVideo() { + final c = _previewVideo; + _previewVideo = null; + _previewVideoFailed = false; + if (c != null) { + unawaited(_pauseThenDispose(c)); + } + } + + Future _tryInitPreviewVideo() async { + final playUrl = _previewPlayUrl(widget.template); + if (playUrl == null || !_urlLooksLikeVideo(playUrl)) return; + final uri = Uri.tryParse(playUrl); + if (uri == null) { + if (mounted) setState(() => _previewVideoFailed = true); + return; + } + final c = VideoPlayerController.networkUrl(uri) + ..setLooping(true) + ..setVolume(1); + try { + await c.initialize(); + await c.play(); + if (!mounted) { + await c.dispose(); + return; + } + setState(() => _previewVideo = c); + } catch (_) { + await c.dispose(); + if (mounted) setState(() => _previewVideoFailed = true); + } + } + + @override + void initState() { + super.initState(); + unawaited(_tryInitPreviewVideo()); + } + + @override + void didUpdateWidget(covariant GenerateScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.template?.videoUrl != widget.template?.videoUrl || + oldWidget.template?.image != widget.template?.image) { + _disposePreviewVideo(); + unawaited(_tryInitPreviewVideo()); + } + } + + @override + void dispose() { + _disposePreviewVideo(); + super.dispose(); + } + int get _estimatedCost { - final c = widget.template?.cost ?? 0; - return c > 0 ? c : 20; + const fallback = 20; + final t = widget.template; + if (t == null) return fallback; + + int effective720() { + if (t.cost720p != null && t.cost720p! > 0) return t.cost720p!; + if (t.cost > 0) return t.cost; + return fallback; + } + + if (_outputSize == '480p') { + if (t.cost480p != null && t.cost480p! > 0) return t.cost480p!; + final h = effective720(); + return max(1, (h / 2).round()); + } + final c = effective720(); + return c > 0 ? c : fallback; } @override @@ -305,14 +441,14 @@ class _GenerateScreenState extends State { children: [ _resoChip( '480p', - _heatmap == '480p', - () => setState(() => _heatmap = '480p'), + _outputSize == '480p', + () => setState(() => _outputSize = '480p'), ), const SizedBox(width: 12), _resoChip( '720p', - _heatmap == '720p', - () => setState(() => _heatmap = '720p'), + _outputSize == '720p', + () => setState(() => _outputSize = '720p'), ), ], ), @@ -458,7 +594,7 @@ class _GenerateScreenState extends State { children: [ ClipRRect( borderRadius: BorderRadius.circular(innerR), - child: _buildPreviewImageLayer(url, fix), + child: _buildPreviewMediaLayer(url, fix), ), Positioned.fill( child: IgnorePointer( @@ -479,7 +615,60 @@ class _GenerateScreenState extends State { ); } - Widget _buildPreviewImageLayer(String url, String? fix) { + /// 视频预览:与 [GenerateResultScreen] / 首页背景一致,循环播放;失败则回退静态图。 + Widget _buildPreviewMediaLayer(String url, String? fix) { + if (_previewIsVideo) { + if (_previewVideoFailed) { + return _buildPreviewStaticImageLayer(url, fix); + } + final c = _previewVideo; + if (c == null || !c.value.isInitialized) { + return _previewPlaceholder(loading: true); + } + return LayoutBuilder( + builder: (context, constraints) { + final cw = constraints.maxWidth; + final ch = constraints.maxHeight; + final w = c.value.size.width; + final h = c.value.size.height; + if (w <= 0 || + h <= 0 || + !cw.isFinite || + !ch.isFinite || + cw <= 0 || + ch <= 0) { + return _previewPlaceholder(loading: true); + } + return Stack( + fit: StackFit.expand, + children: [ + Positioned.fill( + child: ClipRect( + child: SizedBox( + width: cw, + height: ch, + child: FittedBox( + fit: BoxFit.cover, + alignment: Alignment.center, + clipBehavior: Clip.none, + child: SizedBox( + width: w, + height: h, + child: VideoPlayer(c), + ), + ), + ), + ), + ), + ], + ); + }, + ); + } + return _buildPreviewStaticImageLayer(url, fix); + } + + Widget _buildPreviewStaticImageLayer(String url, String? fix) { if (url.isNotEmpty) { return CachedNetworkImage( imageUrl: url, @@ -669,3 +858,10 @@ class _GenerateScreenState extends State { ); } } + +bool _urlLooksLikeVideo(String url) { + if (url.isEmpty) return false; + final lower = url.toLowerCase(); + const hints = ['.mp4', '.m3u8', '.webm', '.mov', '.mkv', '.avi']; + return hints.any((h) => lower.contains(h)); +} diff --git a/lib/features/history/credit_record_tab.dart b/lib/features/history/credit_record_tab.dart index 6475ef9..98a192a 100644 --- a/lib/features/history/credit_record_tab.dart +++ b/lib/features/history/credit_record_tab.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; +import '../../core/auth/auth_service.dart'; import '../../design/pencil_theme.dart'; -/// WBRp4「Credit Record」内容区样式。 +/// WBRp4「Credit Record」内容区 — 对齐 `funymee_home.pen` [listCr](ez9wP)。 class CreditRecordTab extends StatefulWidget { const CreditRecordTab({super.key}); @@ -17,11 +18,27 @@ class _CreditRecordTabState extends State { bool _loading = true; String? _error; List _records = []; + VoidCallback? _cancelLoginWait; @override void initState() { super.initState(); - _load(); + _cancelLoginWait = AuthService.whenLoginSucceeded( + onReady: _load, + onFailed: () { + if (!mounted) return; + setState(() { + _loading = false; + _error = 'Sign in failed'; + }); + }, + ); + } + + @override + void dispose() { + _cancelLoginWait?.call(); + super.dispose(); } Future _load() async { @@ -44,18 +61,20 @@ class _CreditRecordTabState extends State { }); } - String _formatTime(int? t) { + String _formatDate(int? t) { if (t == null) return '—'; var ms = t; if (t < 2000000000) ms = t * 1000; final dt = DateTime.fromMillisecondsSinceEpoch(ms); - return DateFormat('yyyy-MM-dd HH:mm').format(dt); + return DateFormat('yyyy/MM/dd').format(dt); } @override Widget build(BuildContext context) { if (_loading) { - return const Center(child: CircularProgressIndicator()); + return const Center( + child: CircularProgressIndicator(color: PencilTheme.underlineGold), + ); } if (_error != null) { return Center( @@ -70,55 +89,103 @@ class _CreditRecordTabState extends State { } if (_records.isEmpty) { return Center( - child: Text('No records.', - style: GoogleFonts.inter(color: PencilTheme.inkSoft)), + child: Text( + 'No records.', + style: GoogleFonts.inter(color: PencilTheme.inkSoft), + ), ); } return RefreshIndicator( + color: PencilTheme.underlineGold, onRefresh: _load, child: ListView.separated( - padding: const EdgeInsets.fromLTRB(16, 4, 16, 28), + padding: const EdgeInsets.fromLTRB(16, 8, 16, 28), itemCount: _records.length, - separatorBuilder: (_, _) => const SizedBox(height: 8), + separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (_, i) { final r = _records[i]; final c = r.credits ?? 0; - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: PencilTheme.genHintBorder), - boxShadow: [ - BoxShadow( - color: const Color(0x30CA8A04), - blurRadius: 20, - offset: const Offset(0, 6), - ), - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${c > 0 ? '+' : ''}$c credits', - style: GoogleFonts.inter( - fontWeight: FontWeight.w700, - color: PencilTheme.stone900, - ), - ), - Text( - _formatTime(r.createTime), - style: GoogleFonts.inter( - fontSize: 13, - color: PencilTheme.stone600, - ), - ), - ], - ), + return _CreditRecordRowCard( + amountLabel: '${c > 0 ? '+' : ''}$c', + dateLabel: _formatDate(r.createTime), ); }, ), ); } } + +/// 单条流水:72 高、圆角 10、黄橙渐变 + 纹理,左 sparkles + 金额,右日期。 +class _CreditRecordRowCard extends StatelessWidget { + const _CreditRecordRowCard({ + required this.amountLabel, + required this.dateLabel, + }); + + final String amountLabel; + final String dateLabel; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + height: 72, + child: Stack( + fit: StackFit.expand, + children: [ + Container(decoration: const BoxDecoration(gradient: PencilTheme.creditRecordRowGradient)), + Positioned.fill( + child: IgnorePointer( + child: Opacity( + opacity: 0.45, + child: Image.asset( + 'assets/images/card_texture_glow_lines.png', + fit: BoxFit.cover, + errorBuilder: (_, _, _) => const SizedBox.shrink(), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.auto_awesome, + size: 22, + color: Colors.white, + ), + const SizedBox(width: 10), + Text( + amountLabel, + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ], + ), + Text( + dateLabel, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/history/history_screen.dart b/lib/features/history/history_screen.dart index 8aab737..2d12d82 100644 --- a/lib/features/history/history_screen.dart +++ b/lib/features/history/history_screen.dart @@ -3,14 +3,28 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../core/app_env.dart'; +import '../../core/auth/auth_service.dart'; +import '../../core/user/user_state.dart'; import '../../design/pencil_theme.dart'; import '../../widgets/pencil_chrome.dart'; +import '../generate/generate_result_screen.dart'; import 'credit_record_tab.dart'; import 'widgets/history_grid_card.dart'; /// `WBRp4` My History — 黄→白渐变、顶栏、双 Tab、24h 提示、双列卡片。 class HistoryScreen extends StatefulWidget { - const HistoryScreen({super.key}); + const HistoryScreen({ + super.key, + this.isRootTab = false, + this.isTabSelected = true, + }); + + /// When true (e.g. bottom tab), hide back button; when pushed, show back. + final bool isRootTab; + + /// Bottom shell: only `true` when the History tab is selected — defers `my-tasks` load. + /// Pushed routes should default `true` so load runs as before. + final bool isTabSelected; @override State createState() => _HistoryScreenState(); @@ -18,14 +32,47 @@ class HistoryScreen extends StatefulWidget { class _HistoryScreenState extends State { int _tab = 0; - bool _loading = true; + bool _loading = false; String? _error; List _items = []; Map _localCovers = {}; + VoidCallback? _cancelLoginWait; @override void initState() { super.initState(); + _cancelLoginWait = AuthService.whenLoginSucceeded( + onReady: _tryLoadTasks, + onFailed: () { + if (!mounted || !widget.isTabSelected) return; + setState(() { + _loading = false; + _error = 'Sign in failed'; + }); + }, + ); + } + + @override + void didUpdateWidget(HistoryScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (!oldWidget.isTabSelected && widget.isTabSelected) { + _tryLoadTasks(); + } + } + + @override + void dispose() { + _cancelLoginWait?.call(); + super.dispose(); + } + + /// `GET /v1/image/my-tasks` only when this tab is visible and login succeeded. + void _tryLoadTasks() { + if (!widget.isTabSelected) return; + if (!AuthService.isLoginComplete.value) return; + final uid = UserState.userId.value; + if (uid == null || uid.isEmpty) return; _load(); } @@ -77,9 +124,12 @@ class _HistoryScreenState extends State { const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ - PencilRoundBackButton( - onPressed: () => Navigator.of(context).pop(), - ), + if (widget.isRootTab) + const SizedBox(width: 44) + else + PencilRoundBackButton( + onPressed: () => Navigator.of(context).pop(), + ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -140,6 +190,26 @@ class _HistoryScreenState extends State { } Widget _myHistoryBody() { + if (!widget.isTabSelected) { + return const SizedBox.shrink(); + } + if (!AuthService.isLoginComplete.value) { + return const Center(child: CircularProgressIndicator()); + } + final uid = UserState.userId.value; + if (uid == null || uid.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _error ?? 'Sign in failed', + textAlign: TextAlign.center, + ), + ], + ), + ); + } if (_loading) { return const Center(child: CircularProgressIndicator()); } @@ -187,6 +257,16 @@ class _HistoryScreenState extends State { item: t, localCoverPath: id.isEmpty ? null : _localCovers[id], + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => GenerateResultScreen( + taskId: id, + resultUrl: t.resultUrl?.trim() ?? '', + ), + ), + ); + }, onDownload: () {}, ); }, diff --git a/lib/features/history/widgets/history_grid_card.dart b/lib/features/history/widgets/history_grid_card.dart index 4305060..5a400a2 100644 --- a/lib/features/history/widgets/history_grid_card.dart +++ b/lib/features/history/widgets/history_grid_card.dart @@ -4,26 +4,33 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:intl/intl.dart'; import '../../../design/pencil_theme.dart'; +final _historyCardDateFormat = DateFormat('MMM d, yyyy'); + /// WBRp4 单张卡片:171×182 比例,圆角 20,Download pill。 +/// +/// [MyTaskItem.resultUrl] 为生成结果地址(逻辑字段 `imgUrl`,线网常为 `boost`),可能是图片或视频。 class HistoryGridCard extends StatelessWidget { const HistoryGridCard({ super.key, required this.item, this.localCoverPath, + this.onTap, this.onDownload, }); final MyTaskItem item; final String? localCoverPath; + final VoidCallback? onTap; final VoidCallback? onDownload; @override Widget build(BuildContext context) { final url = item.resultUrl?.trim() ?? ''; - final created = item.createTime ?? '—'; + final dateLabel = _formatCardDate(item.createTime); final remainder = _remainderLabel(item.createTime); return LayoutBuilder( @@ -33,9 +40,12 @@ class HistoryGridCard extends StatelessWidget { return SizedBox( width: w, height: h, - child: Stack( - clipBehavior: Clip.none, - children: [ + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Stack( + clipBehavior: Clip.none, + children: [ Positioned( left: 0, top: 0, @@ -43,44 +53,81 @@ class HistoryGridCard extends StatelessWidget { height: h, child: ClipRRect( borderRadius: BorderRadius.circular(20), - child: url.isNotEmpty - ? CachedNetworkImage(imageUrl: url, fit: BoxFit.cover) - : localCoverPath != null - ? Image.file(File(localCoverPath!), fit: BoxFit.cover) - : Container(color: PencilTheme.cardThumbBg), + child: _HistoryThumb( + networkUrl: url.isNotEmpty ? url : null, + localPath: localCoverPath, + ), ), ), Positioned( - left: 8, + left: 0, + right: 0, + top: 0, + height: 64, + child: ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.55), + Colors.black.withValues(alpha: 0), + ], + ), + ), + ), + ), + ), + Positioned( + left: 10, top: 10, - right: 8, + right: 10, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - created, + dateLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: GoogleFonts.inter( fontSize: 9, fontWeight: FontWeight.w500, - color: PencilTheme.inkMuted, + color: Colors.white.withValues(alpha: 0.95), + shadows: const [ + Shadow( + blurRadius: 4, + color: Color(0x40000000), + offset: Offset(0, 1), + ), + ], ), ), + const SizedBox(height: 2), Text( remainder, style: GoogleFonts.inter( fontSize: 9, fontWeight: FontWeight.w600, color: PencilTheme.underlineGold, + shadows: const [ + Shadow( + blurRadius: 4, + color: Color(0x40000000), + offset: Offset(0, 1), + ), + ], ), ), ], ), ), Positioned( - right: 0, - bottom: 0, + right: 8, + bottom: 8, child: Material( color: Colors.white, shape: RoundedRectangleBorder( @@ -114,6 +161,7 @@ class HistoryGridCard extends StatelessWidget { ), ), ], + ), ), ); }, @@ -121,17 +169,85 @@ class HistoryGridCard extends StatelessWidget { } } -String _remainderLabel(String? createTimeRaw) { - if (createTimeRaw == null || createTimeRaw.isEmpty) return '—'; - DateTime? created; - final asInt = int.tryParse(createTimeRaw); +bool _urlLooksLikeVideo(String url) { + if (url.isEmpty) return false; + final lower = url.toLowerCase(); + const hints = ['.mp4', '.m3u8', '.webm', '.mov', '.mkv', '.avi']; + return hints.any((h) => lower.contains(h)); +} + +/// 图片:网络图;视频:仅封面(优先本地上传缩略图),不播放视频。 +class _HistoryThumb extends StatelessWidget { + const _HistoryThumb({ + this.networkUrl, + this.localPath, + }); + + final String? networkUrl; + final String? localPath; + + @override + Widget build(BuildContext context) { + final net = networkUrl?.trim() ?? ''; + if (net.isNotEmpty) { + final uri = Uri.tryParse(net); + if (uri != null && + (uri.isScheme('http') || uri.isScheme('https'))) { + if (_urlLooksLikeVideo(net)) { + if (localPath != null && localPath!.isNotEmpty) { + return Image.file(File(localPath!), fit: BoxFit.cover); + } + return const _VideoCoverPlaceholder(); + } + return CachedNetworkImage(imageUrl: net, fit: BoxFit.cover); + } + } + if (localPath != null && localPath!.isNotEmpty) { + return Image.file(File(localPath!), fit: BoxFit.cover); + } + return Container(color: PencilTheme.cardThumbBg); + } +} + +/// 无本地封面时的视频任务占位(不拉起解码器)。 +class _VideoCoverPlaceholder extends StatelessWidget { + const _VideoCoverPlaceholder(); + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: PencilTheme.cardThumbBg, + child: Center( + child: Icon( + Icons.videocam_outlined, + size: 40, + color: PencilTheme.inkSoft.withValues(alpha: 0.45), + ), + ), + ); + } +} + +/// 与 [MyTaskItem.createTime] 一致:毫秒时间戳、秒级时间戳或 ISO 字符串。 +DateTime? _parseCreateTime(String? raw) { + if (raw == null || raw.isEmpty) return null; + final asInt = int.tryParse(raw.trim()); if (asInt != null) { var ms = asInt; if (asInt < 2000000000) ms = asInt * 1000; - created = DateTime.fromMillisecondsSinceEpoch(ms); - } else { - created = DateTime.tryParse(createTimeRaw); + return DateTime.fromMillisecondsSinceEpoch(ms); } + return DateTime.tryParse(raw.trim()); +} + +String _formatCardDate(String? raw) { + final dt = _parseCreateTime(raw); + if (dt == null) return '—'; + return _historyCardDateFormat.format(dt); +} + +String _remainderLabel(String? createTimeRaw) { + final created = _parseCreateTime(createTimeRaw); if (created == null) return '—'; final deadline = created.add(const Duration(hours: 24)); final left = deadline.difference(DateTime.now()); diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart index b46683f..2dd7db0 100644 --- a/lib/features/home/home_screen.dart +++ b/lib/features/home/home_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' show max; import 'package:cached_network_image/cached_network_image.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart'; @@ -12,8 +13,21 @@ import '../../core/user/user_state.dart'; import '../../design/pencil_theme.dart'; import '../../widgets/pencil_chrome.dart'; import '../generate/generate_screen.dart'; -import '../history/history_screen.dart'; -import '../profile/profile_screen.dart'; + +/// 首页 Create Now 上方展示的预估积分:**480p 档**(与 [GenerateScreen] 选 480p 时一致)。 +int _homeCostDisplay480p(ExtConfigItem? t) { + if (t == null) return 0; + if (t.cost480p != null && t.cost480p! > 0) return t.cost480p!; + int effective720() { + if (t.cost720p != null && t.cost720p! > 0) return t.cost720p!; + if (t.cost > 0) return t.cost; + return 0; + } + + final h = effective720(); + if (h <= 0) return 0; + return max(1, (h / 2).round()); +} /// 首页横向 [PageView] 的一页:对应某个顶部分类 [tabIndex],[item] 为空表示该分类暂无模板占位。 class _FlatHomePage { @@ -356,7 +370,7 @@ class _HomeScreenState extends State { padding: const EdgeInsets.only( left: 16, right: 16, - bottom: 34, + bottom: 16, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -364,42 +378,15 @@ class _HomeScreenState extends State { SizedBox( height: 56, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.end, children: [ - PencilGlassSquareButton( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const HistoryScreen(), - ), + ValueListenableBuilder( + valueListenable: UserState.credits, + builder: (_, credits, _) { + return PencilGlassCreditsPill( + amountText: credits.toStringAsFixed(2), ); }, - child: const Icon(Icons.history_rounded, - color: Colors.white, size: 22), - ), - Row( - children: [ - ValueListenableBuilder( - valueListenable: UserState.credits, - builder: (_, credits, _) { - return PencilGlassCreditsPill( - amountText: credits.toStringAsFixed(2), - ); - }, - ), - const SizedBox(width: 10), - PencilGlassSquareButton( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const ProfileScreen(), - ), - ); - }, - child: const Icon(Icons.settings_rounded, - color: Colors.white, size: 22), - ), - ], ), ], ), @@ -661,7 +648,7 @@ class _HomeScreenState extends State { final template = flat.isEmpty ? null : flat[safe].item; - final cost = template?.cost ?? 0; + final cost = _homeCostDisplay480p(template); return Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/features/profile/profile_screen.dart b/lib/features/profile/profile_screen.dart index e3f7e05..1a11d29 100644 --- a/lib/features/profile/profile_screen.dart +++ b/lib/features/profile/profile_screen.dart @@ -14,7 +14,10 @@ import 'delete_account_flow.dart'; /// `5J8Po` 个人中心。 class ProfileScreen extends StatefulWidget { - const ProfileScreen({super.key}); + const ProfileScreen({super.key, this.isRootTab = false}); + + /// When true (e.g. bottom tab), hide close; when pushed, show close. + final bool isRootTab; @override State createState() => _ProfileScreenState(); @@ -47,12 +50,23 @@ class _ProfileScreenState extends State { padding: const EdgeInsets.fromLTRB(2, 0, 14, 10), child: SizedBox( height: 56, - child: Align( - alignment: Alignment.centerRight, - child: PencilRoundCloseButton( - onPressed: () => Navigator.of(context).pop(), - ), - ), + child: widget.isRootTab + ? Center( + child: Text( + 'Profile', + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: PencilTheme.ink, + ), + ), + ) + : Align( + alignment: Alignment.centerRight, + child: PencilRoundCloseButton( + onPressed: () => Navigator.of(context).pop(), + ), + ), ), ), Expanded( diff --git a/lib/features/purchase/purchase_screen.dart b/lib/features/purchase/purchase_screen.dart index f224139..66e9f4d 100644 --- a/lib/features/purchase/purchase_screen.dart +++ b/lib/features/purchase/purchase_screen.dart @@ -459,9 +459,11 @@ class _ProductCard extends StatelessWidget { @override Widget build(BuildContext context) { final rawTitle = item.title; - final title = (rawTitle != null && rawTitle.trim().isNotEmpty) - ? rawTitle - : 'Credit'; + final creditsTopLabel = item.credits != null + ? 'Credits:${item.credits}' + : (rawTitle != null && rawTitle.trim().isNotEmpty) + ? 'Credits:${rawTitle.trim()}' + : 'Credits:—'; final actual = item.actualAmount ?? '—'; final origin = item.originAmount; final bonus = item.bonus; @@ -474,7 +476,7 @@ class _ProductCard extends StatelessWidget { onTap: paying ? null : () => onTap(item, index), borderRadius: BorderRadius.circular(10), child: SizedBox( - height: 125, + height: 130, child: Stack( clipBehavior: Clip.none, children: [ @@ -484,7 +486,7 @@ class _ProductCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - title, + creditsTopLabel, style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, diff --git a/lib/features/shell/main_screen.dart b/lib/features/shell/main_screen.dart index 04bff35..24e8579 100644 --- a/lib/features/shell/main_screen.dart +++ b/lib/features/shell/main_screen.dart @@ -1,13 +1,55 @@ import 'package:flutter/material.dart'; +import '../history/history_screen.dart'; import '../home/home_screen.dart'; +import '../profile/profile_screen.dart'; -/// 根壳:设计稿首页无底部 Tab,子页由首页按钮/路由 [Navigator.push] 进入。 -class MainScreen extends StatelessWidget { +/// Root shell: bottom tabs **Home**, **History**, **Profile** (English labels). +class MainScreen extends StatefulWidget { const MainScreen({super.key}); + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + int _index = 0; + @override Widget build(BuildContext context) { - return const HomeScreen(); + return Scaffold( + body: IndexedStack( + index: _index, + children: [ + const HomeScreen(), + HistoryScreen( + isRootTab: true, + isTabSelected: _index == 1, + ), + const ProfileScreen(isRootTab: true), + ], + ), + bottomNavigationBar: NavigationBar( + selectedIndex: _index, + onDestinationSelected: (i) => setState(() => _index = i), + destinations: const [ + NavigationDestination( + icon: Icon(Icons.home_outlined), + selectedIcon: Icon(Icons.home_rounded), + label: 'Home', + ), + NavigationDestination( + icon: Icon(Icons.history_outlined), + selectedIcon: Icon(Icons.history_rounded), + label: 'History', + ), + NavigationDestination( + icon: Icon(Icons.person_outline_rounded), + selectedIcon: Icon(Icons.person_rounded), + label: 'Profile', + ), + ], + ), + ); } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8845aa7..d4a162a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import device_info_plus import file_selector_macos +import gal import in_app_purchase_storekit import package_info_plus import shared_preferences_foundation @@ -17,6 +18,7 @@ import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 1a632e4..68db174 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -295,6 +295,14 @@ packages: description: flutter source: sdk version: "0.0.0" + gal: + dependency: "direct main" + description: + name: gal + sha256: "969598f986789127fd407a750413249e1352116d4c2be66e81837ffeeaafdfee" + url: "https://pub.dev" + source: hosted + version: "2.3.2" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7299035..8676f96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: google_fonts: ^6.2.1 package_info_plus: ^8.1.2 webview_flutter: ^4.13.1 + gal: ^2.3.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 77ab7a0..c541e13 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + GalPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GalPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 277a56f..9478e5f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + gal ) list(APPEND FLUTTER_FFI_PLUGIN_LIST